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
+277
View File
@@ -0,0 +1,277 @@
<?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/>.
/**
* Prepares PHPUnit environment, the phpunit.xml configuration
* must specify this file as bootstrap.
*
* Exit codes: {@see phpunit_bootstrap_error()}
*
* @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
*/
// phpcs:disable moodle.Files.MoodleInternal.MoodleInternalGlobalState
if (isset($_SERVER['REMOTE_ADDR'])) {
die; // No access from web!
}
// We want to know about all problems.
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
// Make sure OPcache does not strip comments, we need them in phpunit!
if (ini_get('opcache.enable') && strtolower(ini_get('opcache.enable')) !== 'off') {
if (!ini_get('opcache.save_comments') || strtolower(ini_get('opcache.save_comments')) === 'off') {
ini_set('opcache.enable', 0);
}
}
if (!defined('IGNORE_COMPONENT_CACHE')) {
define('IGNORE_COMPONENT_CACHE', true);
}
require_once(__DIR__ . '/bootstraplib.php');
require_once(__DIR__ . '/../testing/lib.php');
if (isset($_SERVER['REMOTE_ADDR'])) {
phpunit_bootstrap_error(1, 'Unit tests can be executed only from command line!');
}
if (defined('PHPUNIT_TEST')) {
phpunit_bootstrap_error(1, "PHPUNIT_TEST constant must not be manually defined anywhere!");
}
/** PHPUnit testing framework active */
define('PHPUNIT_TEST', true);
if (!defined('PHPUNIT_UTIL')) {
/** Identifies utility scripts - the database does not need to be initialised */
define('PHPUNIT_UTIL', false);
}
if (defined('CLI_SCRIPT')) {
phpunit_bootstrap_error(1, 'CLI_SCRIPT must not be manually defined in any PHPUnit test scripts');
}
define('CLI_SCRIPT', true);
$phpunitversion = PHPUnit\Runner\Version::id();
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
if ($phpunitversion === '@package_version@') {
// Library checked out from git, let's hope dev knows that 3.6.0 is required.
} else if (version_compare($phpunitversion, '3.6.0', 'lt')) {
phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITWRONG, $phpunitversion);
}
unset($phpunitversion);
// Only load CFG from config.php, stop ASAP in lib/setup.php.
define('ABORT_AFTER_CONFIG', true);
require(__DIR__ . '/../../config.php');
if (!defined('PHPUNIT_LONGTEST')) {
/** Execute longer version of tests */
define('PHPUNIT_LONGTEST', false);
}
// Remove error handling overrides done in config.php.
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
set_time_limit(0); // No time limit in CLI scripts, user may cancel execution.
// Prepare dataroot.
umask(0);
if (isset($CFG->phpunit_directorypermissions)) {
$CFG->directorypermissions = $CFG->phpunit_directorypermissions;
} else {
$CFG->directorypermissions = 02777;
}
$CFG->filepermissions = ($CFG->directorypermissions & 0666);
if (!isset($CFG->phpunit_dataroot)) {
phpunit_bootstrap_error(
PHPUNIT_EXITCODE_CONFIGERROR,
'Missing $CFG->phpunit_dataroot in config.php, can not run tests!',
);
}
// Create test dir if does not exists yet.
if (!file_exists($CFG->phpunit_dataroot)) {
mkdir($CFG->phpunit_dataroot, $CFG->directorypermissions);
}
if (!is_dir($CFG->phpunit_dataroot)) {
phpunit_bootstrap_error(
PHPUNIT_EXITCODE_CONFIGERROR,
'$CFG->phpunit_dataroot directory can not be created, can not run tests!',
);
}
// Ensure we access to phpunit_dataroot realpath always.
$CFG->phpunit_dataroot = realpath($CFG->phpunit_dataroot);
if (isset($CFG->dataroot) && $CFG->phpunit_dataroot === $CFG->dataroot) {
phpunit_bootstrap_error(
PHPUNIT_EXITCODE_CONFIGERROR,
'$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!',
);
}
if (!is_writable($CFG->phpunit_dataroot)) {
// Try to fix permissions if possible.
if (function_exists('posix_getuid')) {
$chmod = fileperms($CFG->phpunit_dataroot);
if (fileowner($CFG->phpunit_dataroot) == posix_getuid()) {
$chmod = $chmod | 0700;
chmod($CFG->phpunit_dataroot, $chmod);
}
}
if (!is_writable($CFG->phpunit_dataroot)) {
phpunit_bootstrap_error(
PHPUNIT_EXITCODE_CONFIGERROR,
'$CFG->phpunit_dataroot directory is not writable, can not run tests!',
);
}
}
if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
if ($dh = opendir($CFG->phpunit_dataroot)) {
while (($file = readdir($dh)) !== false) {
if ($file === 'phpunit' || $file === '.' || $file === '..' || $file === '.DS_Store') {
continue;
}
phpunit_bootstrap_error(
PHPUNIT_EXITCODE_CONFIGERROR,
'$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?',
);
}
closedir($dh);
unset($dh);
unset($file);
}
// Now we are 100% sure this dir is used only for phpunit tests.
testing_initdataroot($CFG->phpunit_dataroot, 'phpunit');
}
// Verify db prefix.
if (!isset($CFG->phpunit_prefix)) {
phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Missing $CFG->phpunit_prefix in config.php, can not run tests!');
}
if ($CFG->phpunit_prefix === '') {
phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, '$CFG->phpunit_prefix can not be empty, can not run tests!');
}
if (isset($CFG->prefix) && $CFG->prefix === $CFG->phpunit_prefix) {
phpunit_bootstrap_error(
PHPUNIT_EXITCODE_CONFIGERROR,
'$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!',
);
}
// Override CFG settings if necessary and throw away extra CFG settings.
$CFG->wwwroot = 'https://www.example.com/moodle';
$CFG->dataroot = $CFG->phpunit_dataroot;
$CFG->prefix = $CFG->phpunit_prefix;
$CFG->dbtype = isset($CFG->phpunit_dbtype) ? $CFG->phpunit_dbtype : $CFG->dbtype;
$CFG->dblibrary = isset($CFG->phpunit_dblibrary) ? $CFG->phpunit_dblibrary : $CFG->dblibrary;
$CFG->dbhost = isset($CFG->phpunit_dbhost) ? $CFG->phpunit_dbhost : $CFG->dbhost;
$CFG->dbname = isset($CFG->phpunit_dbname) ? $CFG->phpunit_dbname : $CFG->dbname;
$CFG->dbuser = isset($CFG->phpunit_dbuser) ? $CFG->phpunit_dbuser : $CFG->dbuser;
$CFG->dbpass = isset($CFG->phpunit_dbpass) ? $CFG->phpunit_dbpass : $CFG->dbpass;
$CFG->prefix = isset($CFG->phpunit_prefix) ? $CFG->phpunit_prefix : $CFG->prefix;
$CFG->dboptions = isset($CFG->phpunit_dboptions) ? $CFG->phpunit_dboptions : $CFG->dboptions;
$allowed = ['wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions',
// Keep proxy settings from config.php.
'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass',
'altcacheconfigpath', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot',
'pathtounoconv', 'alternative_file_system_class', 'pathtopython',
];
$productioncfg = (array) $CFG;
$CFG = new stdClass();
foreach ($productioncfg as $key => $value) {
if (!in_array($key, $allowed) && strpos($key, 'phpunit_') !== 0 && strpos($key, 'behat_') !== 0) {
// Ignore.
continue;
}
$CFG->{$key} = $value;
}
unset($key);
unset($value);
unset($allowed);
unset($productioncfg);
// Force the same CFG settings in all sites.
$CFG->debug = (E_ALL | E_STRICT); // Can not use DEBUG_DEVELOPER yet.
$CFG->debugdeveloper = true;
$CFG->debugdisplay = 1;
error_reporting($CFG->debug);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
// Some ugly hacks.
$CFG->themerev = 1;
$CFG->jsrev = 1;
(function () {
// Determine if this test is being run with isolation.
// This is tricky because neither PHPUnit, nor PHP provide an official way to work this out.
// PHPUnit does set a value, but not until later on and we need this earlier.
// PHPUnit runs isolated tests by creating a class on the fly and running it through proc_open as standard input.
// There is no other legitimate reason to run PHPUnit this way that I'm aware of.
// When run in this way, PHP sets the value of $_SERVER['PHP_SELF'] to "Standard input code".
// It has done this since 2016, and it is unlikely to change.
define(
'PHPUNIT_ISOLATED_TEST',
$_SERVER['PHP_SELF'] === 'Standard input code',
);
})();
// Load test case stub classes and other stuff.
require_once("$CFG->dirroot/lib/phpunit/lib.php");
// Finish moodle init.
define('ABORT_AFTER_CONFIG_CANCEL', true);
if (isset($CFG->phpunit_profilingenabled) && $CFG->phpunit_profilingenabled) {
$CFG->profilingenabled = true;
$CFG->profilingincluded = '*';
}
require("$CFG->dirroot/lib/setup.php");
raise_memory_limit(MEMORY_HUGE);
if (PHPUNIT_UTIL) {
// We are not going to do testing, this is 'true' in utility scripts that only init database.
return;
}
// Make sure the hook manager gets initialised before anybody tries to override callbacks,
// this is not using caches intentionally to help with development.
// Note: We cannot use DI at this point in the bootstrap either.
\core\hook\manager::get_instance();
// Is database and dataroot ready for testing?
[$errorcode, $message] = phpunit_util::testing_ready_problem();
// Print some version info.
phpunit_util::bootstrap_moodle_info();
if ($errorcode) {
phpunit_bootstrap_error($errorcode, $message);
}
// Prepare for the first test run - store fresh globals, reset database and dataroot, etc.
phpunit_util::bootstrap_init();
+82
View File
@@ -0,0 +1,82 @@
<?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/>.
/**
* PHPUnit bootstrap function
*
* Note: these functions must be self contained and must not rely on any other library or include
*
* @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
*/
require_once(__DIR__ . '/../testing/lib.php');
define('PHPUNIT_EXITCODE_PHPUNITMISSING', 129);
define('PHPUNIT_EXITCODE_PHPUNITWRONG', 130);
define('PHPUNIT_EXITCODE_PHPUNITEXTMISSING', 131);
define('PHPUNIT_EXITCODE_CONFIGERROR', 135);
define('PHPUNIT_EXITCODE_CONFIGWARNING', 136);
define('PHPUNIT_EXITCODE_INSTALL', 140);
define('PHPUNIT_EXITCODE_REINSTALL', 141);
/**
* Print error and stop execution
* @param int $errorcode The exit error code
* @param string $text An error message to display
* @return void stops code execution with error code
*/
function phpunit_bootstrap_error($errorcode, $text = '') {
switch ($errorcode) {
case 0:
// this is not an error, just print information and exit
break;
case 1:
$text = 'Error: '.$text;
break;
case PHPUNIT_EXITCODE_PHPUNITMISSING:
$text = "Can not find PHPUnit library, to install use: php composer.phar install";
break;
case PHPUNIT_EXITCODE_PHPUNITWRONG:
$text = 'Moodle requires PHPUnit 3.6.x, '.$text.' is not compatible';
break;
case PHPUNIT_EXITCODE_PHPUNITEXTMISSING:
$text = 'Moodle can not find required PHPUnit extension '.$text;
break;
case PHPUNIT_EXITCODE_CONFIGERROR:
$text = "Moodle PHPUnit environment configuration error:\n".$text;
break;
case PHPUNIT_EXITCODE_CONFIGWARNING:
$text = "Moodle PHPUnit environment configuration warning:\n".$text;
break;
case PHPUNIT_EXITCODE_INSTALL:
$path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php');
$text = "Moodle PHPUnit environment is not initialised, please use:\n php $path";
break;
case PHPUNIT_EXITCODE_REINSTALL:
$path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php');
$text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path";
break;
default:
$text = empty($text) ? '' : ': '.$text;
$text = 'Unknown error '.$errorcode.$text;
break;
}
testing_error($errorcode, $text);
}
+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
+40
View File
@@ -0,0 +1,40 @@
<?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/>.
/**
* Moodle PHPUnit integration
*
* @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
*/
// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php!
require_once(__DIR__.'/classes/util.php');
require_once(__DIR__.'/classes/phpunit_dataset.php');
require_once(__DIR__.'/classes/event_mock.php');
require_once(__DIR__.'/classes/event_sink.php');
require_once(__DIR__.'/classes/message_sink.php');
require_once(__DIR__.'/classes/phpmailer_sink.php');
require_once(__DIR__.'/classes/base_testcase.php');
require_once(__DIR__.'/classes/basic_testcase.php');
require_once(__DIR__.'/classes/database_driver_testcase.php');
require_once(__DIR__.'/classes/advanced_testcase.php');
require_once(__DIR__.'/classes/constraint_object_is_equal_with_exceptions.php');
require_once(__DIR__.'/../testing/classes/test_lock.php');
require_once(__DIR__.'/../testing/classes/tests_finder.php');
+328
View File
@@ -0,0 +1,328 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:annotation>
<xs:documentation source="https://phpunit.de/documentation.html">
This Schema file defines the rules by which the XML configuration file of PHPUnit 9.5 may be structured.
</xs:documentation>
<xs:appinfo source="https://phpunit.de/documentation.html"/>
</xs:annotation>
<xs:element name="phpunit" type="phpUnitType">
<xs:annotation>
<xs:documentation>Root Element</xs:documentation>
</xs:annotation>
</xs:element>
<xs:complexType name="coverageType">
<xs:all>
<xs:element name="include" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:group ref="pathGroup"/>
</xs:complexType>
</xs:element>
<xs:element name="exclude" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:group ref="pathGroup"/>
</xs:complexType>
</xs:element>
<xs:element name="report" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:group ref="coverageReportGroup"/>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="cacheDirectory" type="xs:anyURI"/>
<xs:attribute name="pathCoverage" type="xs:boolean" default="false"/>
<xs:attribute name="includeUncoveredFiles" type="xs:boolean" default="true"/>
<xs:attribute name="processUncoveredFiles" type="xs:boolean" default="false"/>
<xs:attribute name="ignoreDeprecatedCodeUnits" type="xs:boolean" default="false"/>
<xs:attribute name="disableCodeCoverageIgnore" type="xs:boolean" default="false"/>
</xs:complexType>
<xs:complexType name="loggingType">
<xs:group ref="loggingGroup"/>
</xs:complexType>
<xs:complexType name="groupsType">
<xs:choice>
<xs:sequence>
<xs:element name="include" type="groupType"/>
<xs:element name="exclude" type="groupType" minOccurs="0"/>
</xs:sequence>
<xs:sequence>
<xs:element name="exclude" type="groupType"/>
</xs:sequence>
</xs:choice>
</xs:complexType>
<xs:complexType name="groupType">
<xs:sequence>
<xs:element name="group" type="xs:string" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="extensionsType">
<xs:sequence>
<xs:element name="extension" type="objectType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="listenersType">
<xs:sequence>
<xs:element name="listener" type="objectType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="objectType">
<xs:sequence>
<xs:element name="arguments" minOccurs="0">
<xs:complexType>
<xs:group ref="argumentsGroup"/>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="class" type="xs:string" use="required"/>
<xs:attribute name="file" type="xs:anyURI"/>
</xs:complexType>
<xs:complexType name="arrayType">
<xs:sequence>
<xs:element name="element" type="argumentType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="argumentType">
<xs:group ref="argumentChoice"/>
<xs:attribute name="key" use="required"/>
</xs:complexType>
<xs:group name="argumentsGroup">
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="array" type="arrayType" />
<xs:element name="integer" type="xs:integer" />
<xs:element name="string" type="xs:string" />
<xs:element name="double" type="xs:double" />
<xs:element name="null" />
<xs:element name="object" type="objectType" />
<xs:element name="file" type="xs:anyURI" />
<xs:element name="directory" type="xs:anyURI" />
<xs:element name="boolean" type="xs:boolean" />
</xs:choice>
</xs:sequence>
</xs:group>
<xs:group name="argumentChoice">
<xs:choice>
<xs:element name="array" type="arrayType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="integer" type="xs:integer" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="string" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="double" type="xs:double" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="null" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="object" type="objectType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="file" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="directory" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="boolean" type="xs:boolean" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
</xs:group>
<xs:simpleType name="columnsType">
<xs:union>
<xs:simpleType>
<xs:restriction base="xs:integer"/>
</xs:simpleType>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="max"/>
</xs:restriction>
</xs:simpleType>
</xs:union>
</xs:simpleType>
<xs:group name="pathGroup">
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="directory" type="directoryFilterType"/>
<xs:element name="file" type="fileFilterType"/>
</xs:choice>
</xs:sequence>
</xs:group>
<xs:complexType name="directoryFilterType">
<xs:simpleContent>
<xs:extension base="xs:anyURI">
<xs:attribute type="xs:string" name="prefix" default=""/>
<xs:attribute type="xs:string" name="suffix" default="Test.php"/>
<xs:attributeGroup ref="phpVersionGroup"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="executionOrderType">
<xs:restriction base="xs:string">
<xs:enumeration value="default"/>
<xs:enumeration value="defects"/>
<xs:enumeration value="depends"/>
<xs:enumeration value="depends,defects"/>
<xs:enumeration value="depends,duration"/>
<xs:enumeration value="depends,random"/>
<xs:enumeration value="depends,reverse"/>
<xs:enumeration value="depends,size"/>
<xs:enumeration value="duration"/>
<xs:enumeration value="no-depends"/>
<xs:enumeration value="no-depends,defects"/>
<xs:enumeration value="no-depends,duration"/>
<xs:enumeration value="no-depends,random"/>
<xs:enumeration value="no-depends,reverse"/>
<xs:enumeration value="no-depends,size"/>
<xs:enumeration value="random"/>
<xs:enumeration value="reverse"/>
<xs:enumeration value="size"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="fileFilterType">
<xs:simpleContent>
<xs:extension base="xs:anyURI">
<xs:attributeGroup ref="phpVersionGroup"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:attributeGroup name="phpVersionGroup">
<xs:attribute name="phpVersion" type="xs:string" default="5.3.0"/>
<xs:attribute name="phpVersionOperator" type="xs:string" default="&gt;="/>
</xs:attributeGroup>
<xs:complexType name="phpType">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="includePath" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="ini" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="const" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="var" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="env" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="post" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="get" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="cookie" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="server" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="files" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="request" type="namedValueType" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="namedValueType">
<xs:attribute name="name" use="required" type="xs:string"/>
<xs:attribute name="value" use="required" type="xs:anySimpleType"/>
<xs:attribute name="verbatim" use="optional" type="xs:boolean"/>
<xs:attribute name="force" use="optional" type="xs:boolean"/>
</xs:complexType>
<xs:complexType name="phpUnitType">
<xs:annotation>
<xs:documentation>The main type specifying the document structure</xs:documentation>
</xs:annotation>
<xs:group ref="configGroup"/>
<xs:attributeGroup ref="configAttributeGroup"/>
</xs:complexType>
<xs:attributeGroup name="configAttributeGroup">
<xs:attribute name="backupGlobals" type="xs:boolean" default="false"/>
<xs:attribute name="backupStaticAttributes" type="xs:boolean" default="false"/>
<xs:attribute name="bootstrap" type="xs:anyURI"/>
<xs:attribute name="cacheResult" type="xs:boolean" default="true"/>
<xs:attribute name="cacheResultFile" type="xs:anyURI"/>
<xs:attribute name="colors" type="xs:boolean" default="false"/>
<xs:attribute name="columns" type="columnsType" default="80"/>
<xs:attribute name="convertDeprecationsToExceptions" type="xs:boolean" default="true"/>
<xs:attribute name="convertErrorsToExceptions" type="xs:boolean" default="true"/>
<xs:attribute name="convertNoticesToExceptions" type="xs:boolean" default="true"/>
<xs:attribute name="convertWarningsToExceptions" type="xs:boolean" default="true"/>
<xs:attribute name="forceCoversAnnotation" type="xs:boolean" default="false"/>
<xs:attribute name="printerClass" type="xs:string" default="PHPUnit\TextUI\DefaultResultPrinter"/>
<xs:attribute name="printerFile" type="xs:anyURI"/>
<xs:attribute name="processIsolation" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnDefect" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnError" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnFailure" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnWarning" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnIncomplete" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnRisky" type="xs:boolean" default="false"/>
<xs:attribute name="stopOnSkipped" type="xs:boolean" default="false"/>
<xs:attribute name="failOnEmptyTestSuite" type="xs:boolean" default="false"/>
<xs:attribute name="failOnIncomplete" type="xs:boolean" default="false"/>
<xs:attribute name="failOnRisky" type="xs:boolean" default="false"/>
<xs:attribute name="failOnSkipped" type="xs:boolean" default="false"/>
<xs:attribute name="failOnWarning" type="xs:boolean" default="false"/>
<xs:attribute name="beStrictAboutChangesToGlobalState" type="xs:boolean" default="false"/>
<xs:attribute name="beStrictAboutOutputDuringTests" type="xs:boolean" default="false"/>
<xs:attribute name="beStrictAboutResourceUsageDuringSmallTests" type="xs:boolean" default="false"/>
<xs:attribute name="beStrictAboutTestsThatDoNotTestAnything" type="xs:boolean" default="true"/>
<xs:attribute name="beStrictAboutTodoAnnotatedTests" type="xs:boolean" default="false"/>
<xs:attribute name="beStrictAboutCoversAnnotation" type="xs:boolean" default="false"/>
<xs:attribute name="defaultTimeLimit" type="xs:integer" default="0"/>
<xs:attribute name="enforceTimeLimit" type="xs:boolean" default="false"/>
<xs:attribute name="timeoutForSmallTests" type="xs:integer" default="1"/>
<xs:attribute name="timeoutForMediumTests" type="xs:integer" default="10"/>
<xs:attribute name="timeoutForLargeTests" type="xs:integer" default="60"/>
<xs:attribute name="testSuiteLoaderClass" type="xs:string" default="PHPUnit\Runner\StandardTestSuiteLoader"/>
<xs:attribute name="testSuiteLoaderFile" type="xs:anyURI"/>
<xs:attribute name="defaultTestSuite" type="xs:string" default=""/>
<xs:attribute name="verbose" type="xs:boolean" default="false"/>
<xs:attribute name="testdox" type="xs:boolean" default="false"/>
<xs:attribute name="stderr" type="xs:boolean" default="false"/>
<xs:attribute name="reverseDefectList" type="xs:boolean" default="false"/>
<xs:attribute name="registerMockObjectsFromTestArgumentsRecursively" type="xs:boolean" default="false"/>
<xs:attribute name="extensionsDirectory" type="xs:string"/>
<xs:attribute name="executionOrder" type="executionOrderType" default="default"/>
<xs:attribute name="resolveDependencies" type="xs:boolean" default="true"/>
<xs:attribute name="noInteraction" type="xs:boolean" default="false"/>
</xs:attributeGroup>
<xs:group name="configGroup">
<xs:all>
<xs:element ref="testSuiteFacet" minOccurs="0"/>
<xs:element name="groups" type="groupsType" minOccurs="0"/>
<xs:element name="testdoxGroups" type="groupsType" minOccurs="0"/>
<xs:element name="coverage" type="coverageType" minOccurs="0"/>
<xs:element name="logging" type="loggingType" minOccurs="0"/>
<xs:element name="extensions" type="extensionsType" minOccurs="0"/>
<xs:element name="listeners" type="listenersType" minOccurs="0"/>
<xs:element name="php" type="phpType" minOccurs="0"/>
</xs:all>
</xs:group>
<xs:element name="testSuiteFacet" abstract="true"/>
<xs:element name="testsuite" type="testSuiteType" substitutionGroup="testSuiteFacet"/>
<xs:element name="testsuites" type="testSuitesType" substitutionGroup="testSuiteFacet"/>
<xs:complexType name="testSuitesType">
<xs:sequence>
<xs:element name="testsuite" type="testSuiteType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="testSuiteType">
<xs:sequence>
<xs:group ref="pathGroup"/>
<xs:element name="exclude" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
<xs:group name="coverageReportGroup">
<xs:all>
<xs:element name="clover" type="logToFileType" minOccurs="0"/>
<xs:element name="cobertura" type="logToFileType" minOccurs="0"/>
<xs:element name="crap4j" type="coverageReportCrap4JType" minOccurs="0" />
<xs:element name="html" type="coverageReportHtmlType" minOccurs="0" />
<xs:element name="php" type="logToFileType" minOccurs="0" />
<xs:element name="text" type="coverageReportTextType" minOccurs="0" />
<xs:element name="xml" type="logToDirectoryType" minOccurs="0" />
</xs:all>
</xs:group>
<xs:group name="loggingGroup">
<xs:all>
<xs:element name="junit" type="logToFileType" minOccurs="0" />
<xs:element name="teamcity" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxHtml" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxText" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxXml" type="logToFileType" minOccurs="0" />
<xs:element name="text" type="logToFileType" minOccurs="0"/>
</xs:all>
</xs:group>
<xs:complexType name="logToFileType">
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
</xs:complexType>
<xs:complexType name="logToDirectoryType">
<xs:attribute name="outputDirectory" type="xs:anyURI" use="required"/>
</xs:complexType>
<xs:complexType name="coverageReportCrap4JType">
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
<xs:attribute name="threshold" type="xs:integer"/>
</xs:complexType>
<xs:complexType name="coverageReportHtmlType">
<xs:attribute name="outputDirectory" type="xs:anyURI" use="required"/>
<xs:attribute name="lowUpperBound" type="xs:integer" default="50"/>
<xs:attribute name="highLowerBound" type="xs:integer" default="90"/>
</xs:complexType>
<xs:complexType name="coverageReportTextType">
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
<xs:attribute name="showUncoveredFiles" type="xs:boolean" default="false"/>
<xs:attribute name="showOnlySummary" type="xs:boolean" default="false"/>
</xs:complexType>
</xs:schema>
+58
View File
@@ -0,0 +1,58 @@
PHPUnit testing support in Moodle
==================================
Documentation
-------------
* [Moodle PHPUnit integration](https://moodledev.io/general/development/tools/phpunit)
* [Moodle Writing PHPUnit tests](https://moodledev.io/general/development/tools/phpunit#writing-new-tests)
* [PHPUnit online documentation](http://www.phpunit.de/manual/current/en/)
* [Composer dependency manager](http://getcomposer.org/)
Composer installation
---------------------
Composer is a dependency manager for PHP projects.
It installs PHP libraries into /vendor/ subdirectory inside your moodle dirroot.
1. install Composer - [http://getcomposer.org/doc/00-intro.md](http://getcomposer.org/doc/00-intro.md)
2. install PHUnit and dependencies - go to your Moodle dirroot and execute `php composer.phar install`
Configure your server
---------------------
You need to create a new dataroot directory and specify a separate database prefix for the test environment,
see config-dist.php for more information.
* add `$CFG->phpunit_prefix = 'phpu_';` to your config.php file
* and `$CFG->phpunit_dataroot = '/path/to/phpunitdataroot';` to your config.php file
Initialise the test environment
-------------------------------
Before first execution and after every upgrade the PHPUnit test environment needs to be initialised,
this command also builds the phpunit.xml configuration files.
* execute `php admin/tool/phpunit/cli/init.php`
Execute tests
--------------
* execute `vendor/bin/phpunit` from dirroot directory
* you can execute a single test case class using class name followed by path to test file `vendor/bin/phpunit lib/tests/phpunit_test.php`
* it is also possible to create custom configuration files in xml format and use `vendor/bin/phpunit -c mytestsuites.xml`
How to add more tests?
----------------------
1. create `tests/` directory in your add-on
2. add test file, for example `local/mytest/tests/my_test.php` file with `my_test` class that extends `basic_testcase` or `advanced_testcase`
3. set the test class namespace to that of the class being tested
4. add some `test_*()` methods
5. execute your new test case `vendor/bin/phpunit local/mytest/tests/my_test.php`
6. execute `php admin/tool/phpunit/cli/init.php` to get the plugin tests included in main phpunit.xml configuration file
Windows support
---------------
* use `\` instead of `/` in paths in examples above
+803
View File
@@ -0,0 +1,803 @@
<?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 advanced_testcase extra features.
*
* @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
* @coversDefaultClass \advanced_testcase
*/
class advanced_test extends \advanced_testcase {
public static function setUpBeforeClass(): void {
global $CFG;
require_once(__DIR__ . '/fixtures/adhoc_test_task.php');
}
public function test_debugging(): void {
global $CFG;
$this->resetAfterTest();
debugging('hokus');
$this->assertDebuggingCalled();
debugging('pokus');
$this->assertDebuggingCalled('pokus');
debugging('pokus', DEBUG_MINIMAL);
$this->assertDebuggingCalled('pokus', DEBUG_MINIMAL);
$this->assertDebuggingNotCalled();
debugging('a');
debugging('b', DEBUG_MINIMAL);
debugging('c', DEBUG_DEVELOPER);
$debuggings = $this->getDebuggingMessages();
$this->assertCount(3, $debuggings);
$this->assertSame('a', $debuggings[0]->message);
$this->assertSame(DEBUG_NORMAL, $debuggings[0]->level);
$this->assertSame('b', $debuggings[1]->message);
$this->assertSame(DEBUG_MINIMAL, $debuggings[1]->level);
$this->assertSame('c', $debuggings[2]->message);
$this->assertSame(DEBUG_DEVELOPER, $debuggings[2]->level);
$this->resetDebugging();
$this->assertDebuggingNotCalled();
$debuggings = $this->getDebuggingMessages();
$this->assertCount(0, $debuggings);
set_debugging(DEBUG_NONE);
debugging('hokus');
$this->assertDebuggingNotCalled();
set_debugging(DEBUG_DEVELOPER);
}
/**
* @test
*
* Annotations are a valid PHPUnit method for running tests. Debugging needs to support them.
*/
public function debugging_called_with_annotation() {
debugging('pokus', DEBUG_MINIMAL);
$this->assertDebuggingCalled('pokus', DEBUG_MINIMAL);
}
public function test_set_user(): void {
global $USER, $DB, $SESSION;
$this->resetAfterTest();
$this->assertEquals(0, $USER->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$user = $DB->get_record('user', array('id'=>2));
$this->assertNotEmpty($user);
$this->setUser($user);
$this->assertEquals(2, $USER->id);
$this->assertEquals(2, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$USER->id = 3;
$this->assertEquals(3, $USER->id);
$this->assertEquals(3, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
\core\session\manager::set_user($user);
$this->assertEquals(2, $USER->id);
$this->assertEquals(2, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$USER = $DB->get_record('user', array('id'=>1));
$this->assertNotEmpty($USER);
$this->assertEquals(1, $USER->id);
$this->assertEquals(1, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$this->setUser(null);
$this->assertEquals(0, $USER->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
// Ensure session is reset after setUser, as it may contain extra info.
$SESSION->sometestvalue = true;
$this->setUser($user);
$this->assertObjectNotHasProperty('sometestvalue', $SESSION);
}
public function test_set_admin_user(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$this->assertEquals($USER->id, 2);
$this->assertTrue(is_siteadmin());
}
public function test_set_guest_user(): void {
global $USER;
$this->resetAfterTest();
$this->setGuestUser();
$this->assertEquals($USER->id, 1);
$this->assertTrue(isguestuser());
}
public function test_database_reset(): void {
global $DB;
$this->resetAfterTest();
$this->preventResetByRollback();
$this->assertEquals(1, $DB->count_records('course')); // Only frontpage in new site.
// This is weird table - id is NOT a sequence here.
$this->assertEquals(0, $DB->count_records('context_temp'));
$DB->import_record('context_temp', array('id'=>5, 'path'=>'/1/2', 'depth'=>2));
$record = $DB->get_record('context_temp', array());
$this->assertEquals(5, $record->id);
$this->assertEquals(0, $DB->count_records('user_preferences'));
$originaldisplayid = $DB->insert_record('user_preferences', array('userid'=>2, 'name'=> 'phpunittest', 'value'=>'x'));
$this->assertEquals(1, $DB->count_records('user_preferences'));
$numcourses = $DB->count_records('course');
$course = $this->getDataGenerator()->create_course();
$this->assertEquals($numcourses + 1, $DB->count_records('course'));
$this->assertEquals(2, $DB->count_records('user'));
$DB->delete_records('user', array('id'=>1));
$this->assertEquals(1, $DB->count_records('user'));
$this->resetAllData();
$this->assertEquals(1, $DB->count_records('course')); // Only frontpage in new site.
$this->assertEquals(0, $DB->count_records('context_temp')); // Only frontpage in new site.
$numcourses = $DB->count_records('course');
$course = $this->getDataGenerator()->create_course();
$this->assertEquals($numcourses + 1, $DB->count_records('course'));
$displayid = $DB->insert_record('user_preferences', array('userid'=>2, 'name'=> 'phpunittest', 'value'=>'x'));
$this->assertEquals($originaldisplayid, $displayid);
$this->assertEquals(2, $DB->count_records('user'));
$DB->delete_records('user', array('id'=>2));
$user = $this->getDataGenerator()->create_user();
$this->assertEquals(2, $DB->count_records('user'));
$this->assertGreaterThan(2, $user->id);
$this->resetAllData();
$numcourses = $DB->count_records('course');
$course = $this->getDataGenerator()->create_course();
$this->assertEquals($numcourses + 1, $DB->count_records('course'));
$this->assertEquals(2, $DB->count_records('user'));
$DB->delete_records('user', array('id'=>2));
$this->resetAllData();
$numcourses = $DB->count_records('course');
$course = $this->getDataGenerator()->create_course();
$this->assertEquals($numcourses + 1, $DB->count_records('course'));
$this->assertEquals(2, $DB->count_records('user'));
}
public function test_change_detection(): void {
global $DB, $CFG, $COURSE, $SITE, $USER;
$this->preventResetByRollback();
self::resetAllData(true);
// Database change.
$this->assertEquals(1, $DB->get_field('user', 'confirmed', array('id'=>2)));
$DB->set_field('user', 'confirmed', 0, array('id'=>2));
try {
self::resetAllData(true);
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
}
$this->assertEquals(1, $DB->get_field('user', 'confirmed', array('id'=>2)));
// Config change.
$CFG->xx = 'yy';
unset($CFG->admin);
$CFG->rolesactive = 0;
try {
self::resetAllData(true);
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
$this->assertStringContainsString('xx', $e->getMessage());
$this->assertStringContainsString('admin', $e->getMessage());
$this->assertStringContainsString('rolesactive', $e->getMessage());
}
$this->assertFalse(isset($CFG->xx));
$this->assertTrue(isset($CFG->admin));
$this->assertEquals(1, $CFG->rolesactive);
// _GET change.
$_GET['__somethingthatwillnotnormallybepresent__'] = 'yy';
self::resetAllData(true);
$this->assertEquals(array(), $_GET);
// _POST change.
$_POST['__somethingthatwillnotnormallybepresent2__'] = 'yy';
self::resetAllData(true);
$this->assertEquals(array(), $_POST);
// _FILES change.
$_FILES['__somethingthatwillnotnormallybepresent3__'] = 'yy';
self::resetAllData(true);
$this->assertEquals(array(), $_FILES);
// _REQUEST change.
$_REQUEST['__somethingthatwillnotnormallybepresent4__'] = 'yy';
self::resetAllData(true);
$this->assertEquals(array(), $_REQUEST);
// Silent changes.
$_SERVER['xx'] = 'yy';
self::resetAllData(true);
$this->assertFalse(isset($_SERVER['xx']));
// COURSE change.
$SITE->id = 10;
$COURSE = new \stdClass();
$COURSE->id = 7;
try {
self::resetAllData(true);
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
$this->assertEquals(1, $SITE->id);
$this->assertSame($SITE, $COURSE);
$this->assertSame($SITE, $COURSE);
}
// USER change.
$this->setUser(2);
try {
self::resetAllData(true);
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
$this->assertEquals(0, $USER->id);
}
}
public function test_getDataGenerator(): void {
$generator = $this->getDataGenerator();
$this->assertInstanceOf('testing_data_generator', $generator);
}
public function test_database_mock1(): void {
global $DB;
try {
$DB->get_record('pokus', array());
$this->fail('Exception expected when accessing non existent table');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('dml_exception', $e);
}
$DB = $this->createMock(get_class($DB));
$this->assertNull($DB->get_record('pokus', array()));
// Rest continues after reset.
}
public function test_database_mock2(): void {
global $DB;
// Now the database should be back to normal.
$this->assertFalse($DB->get_record('user', array('id'=>9999)));
}
public function test_assert_time_current(): void {
$this->assertTimeCurrent(time());
$this->setCurrentTimeStart();
$this->assertTimeCurrent(time());
$this->waitForSecond();
$this->assertTimeCurrent(time());
$this->assertTimeCurrent(time()-1);
try {
$this->setCurrentTimeStart();
$this->assertTimeCurrent(time()+10);
$this->fail('Failed assert expected');
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\ExpectationFailedException', $e);
}
try {
$this->setCurrentTimeStart();
$this->assertTimeCurrent(time()-10);
$this->fail('Failed assert expected');
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\ExpectationFailedException', $e);
}
}
/**
* Test the assertEventContextNotUsed() assertion.
*
* Verify that events using the event context in some of their
* methods are detected properly (will throw a warning if they are).
*
* To do so, we'll be using some fixture events (context_used_in_event_xxxx),
* that, on purpose, use the event context (incorrectly) in their methods.
*
* Note that because we are using imported fixture classes, and because we
* are testing for warnings, better we run the tests in a separate process.
*
* @param string $fixture The fixture class to use.
* @param bool $phpwarn Whether a PHP warning is expected.
*
* @runInSeparateProcess
* @dataProvider assert_event_context_not_used_provider
* @covers ::assertEventContextNotUsed
*/
public function test_assert_event_context_not_used($fixture, $phpwarn): void {
require(__DIR__ . '/fixtures/event_fixtures.php');
// Create an event that uses the event context in its get_url() and get_description() methods.
$event = $fixture::create([
'other' => [
'sample' => 1,
'xx' => 10,
],
]);
if ($phpwarn) {
// Let's convert the warnings into an assert-able exception.
set_error_handler(
static function ($errno, $errstr) {
restore_error_handler();
throw new \Exception($errstr, $errno);
},
E_WARNING // Or any other specific E_ that we want to assert.
);
$this->expectException(\Exception::class);
}
$this->assertEventContextNotUsed($event);
}
/**
* Data provider for test_assert_event_context_not_used().
*
* @return array
*/
public static function assert_event_context_not_used_provider(): array {
return [
'correct' => ['\core\event\context_used_in_event_correct', false],
'wrong_get_url' => ['\core\event\context_used_in_event_get_url', true],
'wrong_get_description' => ['\core\event\context_used_in_event_get_description', true],
];
}
public function test_message_processors_reset(): void {
global $DB;
$this->resetAfterTest(true);
// Get all processors first.
$processors1 = get_message_processors();
// Add a new message processor and get all processors again.
$processor = new \stdClass();
$processor->name = 'test_processor';
$processor->enabled = 1;
$DB->insert_record('message_processors', $processor);
$processors2 = get_message_processors();
// Assert that new processor still haven't been added to the list.
$this->assertSame($processors1, $processors2);
// Reset message processors data.
$processors3 = get_message_processors(false, true);
// Now, list of processors should not be the same any more,
// And we should have one more message processor in the list.
$this->assertNotSame($processors1, $processors3);
$this->assertEquals(count($processors1) + 1, count($processors3));
}
public function test_message_redirection(): \phpunit_message_sink {
$this->preventResetByRollback(); // Messaging is not compatible with transactions...
$this->resetAfterTest(false);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Any core message will do here.
$message1 = new \core\message\message();
$message1->courseid = 1;
$message1->component = 'moodle';
$message1->name = 'instantmessage';
$message1->userfrom = $user1;
$message1->userto = $user2;
$message1->subject = 'message subject 1';
$message1->fullmessage = 'message body';
$message1->fullmessageformat = FORMAT_MARKDOWN;
$message1->fullmessagehtml = '<p>message body</p>';
$message1->smallmessage = 'small message';
$message1->notification = 0;
$message2 = new \core\message\message();
$message2->courseid = 1;
$message2->component = 'moodle';
$message2->name = 'instantmessage';
$message2->userfrom = $user2;
$message2->userto = $user1;
$message2->subject = 'message subject 2';
$message2->fullmessage = 'message body';
$message2->fullmessageformat = FORMAT_MARKDOWN;
$message2->fullmessagehtml = '<p>message body</p>';
$message2->smallmessage = 'small message';
$message2->notification = 0;
// There should be debugging message without redirection.
$mailsink = $this->redirectEmails();
$mailsink->close();
message_send($message1);
$this->assertDebuggingCalled(null, null, 'message_send() must print debug message that messaging is disabled in phpunit tests.');
// Sink should catch messages.
$sink = $this->redirectMessages();
$mid1 = message_send($message1);
$mid2 = message_send($message2);
$this->assertDebuggingNotCalled('message redirection must prevent debug messages from the message_send()');
$this->assertEquals(2, $sink->count());
$this->assertGreaterThanOrEqual(1, $mid1);
$this->assertGreaterThanOrEqual($mid1, $mid2);
$messages = $sink->get_messages();
$this->assertIsArray($messages);
$this->assertCount(2, $messages);
$this->assertEquals($mid1, $messages[0]->id);
$this->assertEquals($message1->userto->id, $messages[0]->useridto);
$this->assertEquals($message1->userfrom->id, $messages[0]->useridfrom);
$this->assertEquals($message1->smallmessage, $messages[0]->smallmessage);
$this->assertEquals($mid2, $messages[1]->id);
$this->assertEquals($message2->userto->id, $messages[1]->useridto);
$this->assertEquals($message2->userfrom->id, $messages[1]->useridfrom);
$this->assertEquals($message2->smallmessage, $messages[1]->smallmessage);
// Test resetting.
$sink->clear();
$messages = $sink->get_messages();
$this->assertIsArray($messages);
$this->assertCount(0, $messages);
message_send($message1);
$messages = $sink->get_messages();
$this->assertIsArray($messages);
$this->assertCount(1, $messages);
// Test closing.
$sink->close();
$messages = $sink->get_messages();
$this->assertIsArray($messages);
$this->assertCount(1, $messages, 'Messages in sink are supposed to stay there after close');
// Test debugging is enabled again.
message_send($message1);
$this->assertDebuggingCalled(null, null, 'message_send() must print debug message that messaging is disabled in phpunit tests.');
// Test invalid names and components.
$sink = $this->redirectMessages();
$message3 = new \core\message\message();
$message3->courseid = 1;
$message3->component = 'xxxx_yyyyy';
$message3->name = 'instantmessage';
$message3->userfrom = $user2;
$message3->userto = $user1;
$message3->subject = 'message subject 2';
$message3->fullmessage = 'message body';
$message3->fullmessageformat = FORMAT_MARKDOWN;
$message3->fullmessagehtml = '<p>message body</p>';
$message3->smallmessage = 'small message';
$message3->notification = 0;
$this->assertFalse(message_send($message3));
$this->assertDebuggingCalled('Attempt to send msg from a provider xxxx_yyyyy/instantmessage '.
'that is inactive or not allowed for the user id='.$user1->id);
$message3->component = 'moodle';
$message3->name = 'yyyyyy';
$this->assertFalse(message_send($message3));
$this->assertDebuggingCalled('Attempt to send msg from a provider moodle/yyyyyy '.
'that is inactive or not allowed for the user id='.$user1->id);
message_send($message1);
$this->assertEquals(1, $sink->count());
// Test if sink can be carried over to next test.
$this->assertTrue(\phpunit_util::is_redirecting_messages());
return $sink;
}
/**
* @depends test_message_redirection
*/
public function test_message_redirection_noreset(\phpunit_message_sink $sink): void {
if ($this->isInIsolation()) {
$this->markTestSkipped('State cannot be carried over between tests in isolated tests');
}
$this->preventResetByRollback(); // Messaging is not compatible with transactions...
$this->resetAfterTest();
$this->assertTrue(\phpunit_util::is_redirecting_messages());
$this->assertEquals(1, $sink->count());
$message = new \core\message\message();
$message->courseid = 1;
$message->component = 'moodle';
$message->name = 'instantmessage';
$message->userfrom = get_admin();
$message->userto = get_admin();
$message->subject = 'message subject 1';
$message->fullmessage = 'message body';
$message->fullmessageformat = FORMAT_MARKDOWN;
$message->fullmessagehtml = '<p>message body</p>';
$message->smallmessage = 'small message';
$message->notification = 0;
message_send($message);
$this->assertEquals(2, $sink->count());
}
/**
* @depends test_message_redirection_noreset
*/
public function test_message_redirection_reset(): void {
$this->assertFalse(\phpunit_util::is_redirecting_messages(), 'Test reset must stop message redirection.');
}
public function test_set_timezone(): void {
global $CFG;
$this->resetAfterTest();
$this->assertSame('Australia/Perth', $CFG->timezone);
$this->assertSame('Australia/Perth', date_default_timezone_get());
$this->setTimezone('Pacific/Auckland', 'Europe/Prague');
$this->assertSame('Pacific/Auckland', $CFG->timezone);
$this->assertSame('Pacific/Auckland', date_default_timezone_get());
$this->setTimezone('99', 'Europe/Prague');
$this->assertSame('99', $CFG->timezone);
$this->assertSame('Europe/Prague', date_default_timezone_get());
$this->setTimezone('xxx', 'Europe/Prague');
$this->assertSame('xxx', $CFG->timezone);
$this->assertSame('Europe/Prague', date_default_timezone_get());
$this->setTimezone();
$this->assertSame('Australia/Perth', $CFG->timezone);
$this->assertSame('Australia/Perth', date_default_timezone_get());
try {
$this->setTimezone('Pacific/Auckland', '');
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
}
try {
$this->setTimezone('Pacific/Auckland', 'xxxx');
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
}
try {
$this->setTimezone('Pacific/Auckland', null);
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
}
}
public function test_locale_reset(): void {
global $CFG;
$this->resetAfterTest();
// If this fails self::resetAllData(); must be updated.
$this->assertSame('en_AU.UTF-8', get_string('locale', 'langconfig'));
$this->assertSame('English_Australia.1252', get_string('localewin', 'langconfig'));
if ($CFG->ostype === 'WINDOWS') {
$this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
setlocale(LC_TIME, 'English_USA.1252');
} else {
$this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
setlocale(LC_TIME, 'en_US.UTF-8');
}
try {
self::resetAllData(true);
} catch (\Exception $e) {
$this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e);
}
if ($CFG->ostype === 'WINDOWS') {
$this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
} else {
$this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
}
if ($CFG->ostype === 'WINDOWS') {
$this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
setlocale(LC_TIME, 'English_USA.1252');
} else {
$this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
setlocale(LC_TIME, 'en_US.UTF-8');
}
self::resetAllData(false);
if ($CFG->ostype === 'WINDOWS') {
$this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
} else {
$this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
}
}
/**
* This test sets a user agent and makes sure that it is cleared when the test is reset.
*/
public function test_it_resets_useragent_after_test(): void {
$this->resetAfterTest();
$fakeagent = 'New user agent set.';
// Sanity check: it should not be set when test begins.
self::assertFalse(\core_useragent::get_user_agent_string(), 'It should not be set at first.');
// Set a fake useragent and check it was set properly.
\core_useragent::instance(true, $fakeagent);
self::assertSame($fakeagent, \core_useragent::get_user_agent_string(), 'It should be the forced agent.');
// Reset test data and ansure the useragent was cleaned.
self::resetAllData(false);
self::assertFalse(\core_useragent::get_user_agent_string(), 'It should not be set again, data was reset.');
}
/**
* @covers ::runAdhocTasks
*/
public function test_runadhoctasks_no_tasks_queued(): void {
$this->runAdhocTasks();
$this->expectOutputRegex('/^$/');
}
/**
* @covers ::runAdhocTasks
*/
public function test_runadhoctasks_tasks_queued(): void {
$this->resetAfterTest(true);
$admin = get_admin();
\core\task\manager::queue_adhoc_task(new \core_phpunit\adhoc_test_task());
$this->runAdhocTasks();
$this->expectOutputRegex("/Task was run as {$admin->id}/");
}
/**
* @covers ::runAdhocTasks
*/
public function test_runadhoctasks_with_existing_user_change(): void {
$this->resetAfterTest(true);
$admin = get_admin();
$this->setGuestUser();
\core\task\manager::queue_adhoc_task(new \core_phpunit\adhoc_test_task());
$this->runAdhocTasks();
$this->expectOutputRegex("/Task was run as {$admin->id}/");
}
/**
* @covers ::runAdhocTasks
*/
public function test_runadhoctasks_with_existing_user_change_and_specified(): void {
global $USER;
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setGuestUser();
$task = new \core_phpunit\adhoc_test_task();
$task->set_userid($user->id);
\core\task\manager::queue_adhoc_task($task);
$this->runAdhocTasks();
$this->expectOutputRegex("/Task was run as {$user->id}/");
}
/**
* Test the incrementing mock clock.
*
* @covers ::mock_clock_with_incrementing
* @covers \incrementing_clock
*/
public function test_mock_clock_with_incrementing(): void {
$standard = \core\di::get(\core\clock::class);
$this->assertInstanceOf(\Psr\Clock\ClockInterface::class, $standard);
$this->assertInstanceOf(\core\clock::class, $standard);
$newclock = $this->mock_clock_with_incrementing(0);
$mockedclock = \core\di::get(\core\clock::class);
$this->assertInstanceOf(\incrementing_clock::class, $newclock);
$this->assertSame($newclock, $mockedclock);
// Test the functionality.
$this->assertEquals(0, $mockedclock->now()->getTimestamp());
$this->assertEquals(1, $newclock->now()->getTimestamp());
$this->assertEquals(2, $mockedclock->now()->getTimestamp());
// Specify a specific start time.
$newclock = $this->mock_clock_with_incrementing(12345);
$mockedclock = \core\di::get(\core\clock::class);
$this->assertSame($newclock, $mockedclock);
$this->assertEquals(12345, $mockedclock->now()->getTimestamp());
$this->assertEquals(12346, $newclock->now()->getTimestamp());
$this->assertEquals(12347, $mockedclock->now()->getTimestamp());
$this->assertEquals($newclock->time, $mockedclock->now()->getTimestamp());
}
/**
* Test the incrementing mock clock.
*
* @covers ::mock_clock_with_frozen
* @covers \frozen_clock
*/
public function test_mock_clock_with_frozen(): void {
$standard = \core\di::get(\core\clock::class);
$this->assertInstanceOf(\Psr\Clock\ClockInterface::class, $standard);
$this->assertInstanceOf(\core\clock::class, $standard);
$newclock = $this->mock_clock_with_frozen(0);
$mockedclock = \core\di::get(\core\clock::class);
$this->assertInstanceOf(\frozen_clock::class, $newclock);
$this->assertSame($newclock, $mockedclock);
// Test the functionality.
$initialtime = $mockedclock->now()->getTimestamp();
$this->assertEquals($initialtime, $newclock->now()->getTimestamp());
$this->assertEquals($initialtime, $mockedclock->now()->getTimestamp());
// Specify a specific start time.
$newclock = $this->mock_clock_with_frozen(12345);
$mockedclock = \core\di::get(\core\clock::class);
$this->assertSame($newclock, $mockedclock);
$initialtime = $mockedclock->now();
$this->assertEquals($initialtime, $mockedclock->now());
$this->assertEquals($initialtime, $newclock->now());
$this->assertEquals($initialtime, $mockedclock->now());
}
}
+380
View File
@@ -0,0 +1,380 @@
<?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 phpunit_util;
/**
* Test basic_testcase extra features and PHPUnit Moodle integration.
*
* @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 basic_test extends \basic_testcase {
protected $testassertexecuted = false;
protected function setUp(): void {
parent::setUp();
if ($this->getName() === 'test_setup_assert') {
$this->assertTrue(true);
$this->testassertexecuted = true;
return;
}
}
/**
* Tests that bootstrapping has occurred correctly
* @return void
*/
public function test_bootstrap(): void {
global $CFG;
// The httpswwwroot has been deprecated, we keep it as an alias for backwards compatibility with plugins only.
$this->assertTrue(isset($CFG->httpswwwroot));
$this->assertEquals($CFG->httpswwwroot, $CFG->wwwroot);
$this->assertEquals($CFG->prefix, $CFG->phpunit_prefix);
}
/**
* This is just a verification if I understand the PHPUnit assert docs right --skodak
* @return void
*/
public function test_assert_behaviour(): void {
// Arrays.
$a = array('a', 'b', 'c');
$b = array('a', 'c', 'b');
$c = array('a', 'b', 'c');
$d = array('a', 'b', 'C');
$this->assertNotEquals($a, $b);
$this->assertNotEquals($a, $d);
$this->assertEquals($a, $c);
$this->assertEqualsCanonicalizing($a, $b);
// Objects.
$a = new \stdClass();
$a->x = 'x';
$a->y = 'y';
$b = new \stdClass(); // Switched order.
$b->y = 'y';
$b->x = 'x';
$c = $a;
$d = new \stdClass();
$d->x = 'x';
$d->y = 'y';
$d->z = 'z';
$this->assertEquals($a, $b);
$this->assertNotSame($a, $b);
$this->assertEquals($a, $c);
$this->assertSame($a, $c);
$this->assertNotEquals($a, $d);
// String comparison.
$this->assertEquals(1, '1');
$this->assertEquals(null, '');
$this->assertNotEquals(0, '');
$this->assertNotEquals(null, '0');
$this->assertNotEquals(array(), '');
// Other comparison.
$this->assertEquals(null, null);
$this->assertEquals(false, null);
$this->assertEquals(0, null);
// Emptiness.
$this->assertEmpty(0);
$this->assertEmpty(0.0);
$this->assertEmpty('');
$this->assertEmpty('0');
$this->assertEmpty(false);
$this->assertEmpty(null);
$this->assertEmpty(array());
$this->assertNotEmpty(1);
$this->assertNotEmpty(0.1);
$this->assertNotEmpty(-1);
$this->assertNotEmpty(' ');
$this->assertNotEmpty('0 ');
$this->assertNotEmpty(true);
$this->assertNotEmpty(array(null));
$this->assertNotEmpty(new \stdClass());
}
/**
* Make sure there are no sloppy Windows line endings
* that would break our tests.
*/
public function test_lineendings(): void {
$string = <<<STRING
a
b
STRING;
$this->assertSame("a\nb", $string, 'Make sure all project files are checked out with unix line endings.');
}
/**
* Make sure asserts in setUp() do not create problems.
*/
public function test_setup_assert(): void {
$this->assertTrue($this->testassertexecuted);
$this->testassertexecuted = false;
}
/**
* Test assert Tag
*/
public function test_assert_tag(): void {
// This should succeed.
self::assertTag(['id' => 'testid'], "<div><span id='testid'></span></div>");
$this->expectException(\PHPUnit\Framework\ExpectationFailedException::class);
self::assertTag(['id' => 'testid'], "<div><div>");
}
/**
* Tests for assertEqualsIgnoringWhitespace.
*
* @param string $expected
* @param string $actual
* @param bool $expectationvalid
* @dataProvider equals_ignoring_whitespace_provider
*/
public function test_assertEqualsIgnoringWhitespace( // phpcs:ignore
string $expected,
string $actual,
bool $expectationvalid,
): void {
if (!$expectationvalid) {
$this->expectException(\PHPUnit\Framework\ExpectationFailedException::class);
}
self::assertEqualsIgnoringWhitespace($expected, $actual);
}
/**
* Data provider for assertEqualsIgnoringWhitespace tests
*
* @return array
*/
public static function equals_ignoring_whitespace_provider(): array {
return [
'equal' => ['a b c', 'a b c', true],
'equal with whitespace' => ["a b c", "a\nb c", true],
'equal with extra whitespace' => ["a b c", "a\nb c", true],
'whitespace missing' => ["ab c", "a\nb c", false],
'not equal' => ['a b c', 'a b d', false],
'various space types' => [
implode(' ', [
'20', // Regular space.
"a0", // No-Break Space (NBSP).
"80", // Ogham Space Mark.
"0", // En Quad.
"1", // Em Quad.
"2", // En Space.
"3", // Em Space.
"4", // Three-Per-Em Space.
"5", // Four-Per-Em Space.
"6", // Six-Per-Em Space.
"7", // Figure Space.
"8", // Punctuation Space.
"9", // Thin Space.
"0a", // Hair Space.
"2f", // Narrow No-Break Space (NNBSP).
"5f", // Medium Mathematical Space.
"3000", // Ideographic Space.
".",
]),
implode('', [
// All space chars taken from https://www.compart.com/en/unicode/category/Zs.
"20\u{0020}", // Regular space.
"a0\u{00a0}", // No-Break Space (NBSP).
"80\u{1680}", // Ogham Space Mark.
"0\u{2000}", // En Quad.
"1\u{2001}", // Em Quad.
"2\u{2002}", // En Space.
"3\u{2003}", // Em Space.
"4\u{2004}", // Three-Per-Em Space.
"5\u{2005}", // Four-Per-Em Space.
"6\u{2006}", // Six-Per-Em Space.
"7\u{2007}", // Figure Space.
"8\u{2008}", // Punctuation Space.
"9\u{2009}", // Thin Space.
"0a\u{200a}", // Hair Space.
"2f\u{202f}", // Narrow No-Break Space (NNBSP).
"5f\u{205f}", // Medium Mathematical Space.
"3000\u{3000}", // Ideographic Space.
".",
]),
true,
],
];
}
/**
* Test that a database modification is detected.
*
* @runInSeparateProcess
* @covers \phpunit_util
*/
public function test_db_modification(): void {
global $DB;
$DB->set_field('user', 'confirmed', 1, ['id' => -1]);
// Let's convert the user warnings into an assert-able exception.
set_error_handler(
static function ($errno, $errstr) {
restore_error_handler();
throw new \Exception($errstr, $errno);
},
E_USER_WARNING // Or any other specific E_ that we want to assert.
);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Warning: unexpected database modification');
phpunit_util::reset_all_data(true);
}
/**
* Test that a $CFG modification is detected.
*
* @runInSeparateProcess
* @covers \phpunit_util
*/
public function test_cfg_modification(): void {
global $CFG;
$CFG->xx = 'yy';
unset($CFG->admin);
$CFG->rolesactive = 0;
// Let's convert the user warnings into an assert-able exception.
set_error_handler(
static function ($errno, $errstr) {
restore_error_handler();
throw new \Exception($errstr, $errno);
},
E_USER_WARNING // Or any other specific E_ that we want to assert.
);
$this->expectException(\Exception::class);
$this->expectExceptionMessageMatches('/rolesactive.*xx value.*removal.*admin/ms'); // 3 messages matched.
phpunit_util::reset_all_data(true);
}
/**
* Test that a $USER modification is detected.
*
* @runInSeparateProcess
* @covers \phpunit_util
*/
public function test_user_modification(): void {
global $USER;
$USER->id = 10;
// Let's convert the user warnings into an assert-able exception.
set_error_handler(
static function ($errno, $errstr) {
restore_error_handler();
throw new \Exception($errstr, $errno);
},
E_USER_WARNING // Or any other specific E_ that we want to assert.
);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Warning: unexpected change of $USER');
phpunit_util::reset_all_data(true);
}
/**
* Test that a $COURSE modification is detected.
*
* @runInSeparateProcess
* @covers \phpunit_util
*/
public function test_course_modification(): void {
global $COURSE;
$COURSE->id = 10;
// Let's convert the user warnings into an assert-able exception.
set_error_handler(
static function ($errno, $errstr) {
restore_error_handler();
throw new \Exception($errstr, $errno);
},
E_USER_WARNING // Or any other specific E_ that we want to assert.
);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Warning: unexpected change of $COURSE');
phpunit_util::reset_all_data(true);
}
/**
* Test that all modifications are detected together.
*
* @runInSeparateProcess
* @covers \phpunit_util
*/
public function test_all_modifications(): void {
global $DB, $CFG, $USER, $COURSE;
$DB->set_field('user', 'confirmed', 1, ['id' => -1]);
$CFG->xx = 'yy';
unset($CFG->admin);
$CFG->rolesactive = 0;
$USER->id = 10;
$COURSE->id = 10;
// Let's convert the user warnings into an assert-able exception.
set_error_handler(
static function ($errno, $errstr) {
restore_error_handler();
throw new \Exception($errstr, $errno);
},
E_USER_WARNING // Or any other specific E_ that we want to assert.
);
$this->expectException(\Exception::class);
$this->expectExceptionMessageMatches('/resetting.*rolesactive.*new.*removal.*USER.*COURSE/ms'); // 6 messages matched.
phpunit_util::reset_all_data(true);
}
/**
* Test that an open transaction are managed ok by the reset code (silently rolled back).
*
* @runInSeparateProcess
* @covers \phpunit_util
*/
public function test_transaction_problem(): void {
global $DB, $COURSE;
$originalname = $DB->get_field('course', 'fullname', ['id' => $COURSE->id]); // Normally "PHPUnit test site".
$changedname = 'Ongoing transaction test site';
// Start a transaction and make some database changes.
$DB->start_delegated_transaction();
$DB->set_field('course', 'fullname', $changedname, ['id' => $COURSE->id]);
// Assert that the transaction is open and the changes were made.
$this->assertTrue($DB->is_transaction_started());
$this->assertEquals($changedname, $DB->get_field('course', 'fullname', ['id' => $COURSE->id]));
phpunit_util::reset_all_data(false); // We don't want to detect/warn on database changes for this test.
// Assert that the transaction is now closed and the changes were rolled back.
$this->assertFalse($DB->is_transaction_started());
$this->assertEquals($originalname, $DB->get_field('course', 'fullname', ['id' => $COURSE->id]));
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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_phpunit;
/**
* Fixtures for task 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 adhoc_test_task extends \core\task\adhoc_task {
/**
* Execute.
*/
public function execute() {
global $USER;
mtrace("Task was run as {$USER->id}");
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* Fixtures for advanced_testcase tests.
*
* @package core
* @category event
* @copyright 2024 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com}
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\event;
defined('MOODLE_INTERNAL') || die();
/**
* Event to test that \advanced_testcase::assertEventContextNotUsed() passes ok when no context is used.
*/
class context_used_in_event_correct extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
$this->context = \context_system::instance();
}
public function get_url() {
return new \moodle_url('/somepath/somefile.php'); // No context used.
}
public function get_description() {
return 'Description'; // No context used.
}
}
/**
* Event to test that \advanced_testcase::assertEventContextNotUsed() detects context usage on get_url().
*/
class context_used_in_event_get_url extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
$this->context = \context_system::instance();
}
public function get_url() {
return new \moodle_url('/somepath/somefile.php', ['id' => $this->context->instanceid]); // Causes a PHP Warning.
}
}
/**
* Event to test that \advanced_testcase::assertEventContextNotUsed() detects context usage on get_description().
*/
class context_used_in_event_get_description extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
$this->context = \context_system::instance();
}
public function get_description() {
return $this->context->instanceid . " Description"; // Causes a PHP Warning.
}
}
+3
View File
@@ -0,0 +1,3 @@
id,username,email
5,bozka.novakova,bozka@example.com
7,pepa.novak,pepa@example.com
1 id username email
2 5 bozka.novakova bozka@example.com
3 7 pepa.novak pepa@example.com
+3
View File
@@ -0,0 +1,3 @@
username,email
pepa.novak,pepa@example.com
bozka.novakova,bozka@example.com
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
<row>
<value>7</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
</table>
</dataset>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="course">
<column>id</column>
<column>shortname</column>
<column>fullname</column>
<row>
<value>6</value>
<value>101</value>
<value>1-0-1</value>
</row>
<row>
<value>8</value>
<value>202</value>
<value>2-0-2</value>
</row>
</table>
</dataset>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<column>toolate</column>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>username</column>
<column>email</column>
<row>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
<row>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
</table>
</dataset>
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
<row>
<value>7</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
</table>
<table name="course">
<column>id</column>
<column>shortname</column>
<column>fullname</column>
<row>
<value>6</value>
<value>101</value>
<value>1-0-1</value>
</row>
<row>
<value>8</value>
<value>202</value>
<value>2-0-2</value>
</row>
</table>
</dataset>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
<row>
<value>7</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
</table>
<table name="empty1" />
<table name="empty2">
<column>id</column>
<column>shortname</column>
<column>fullname</column>
</table>
<table name="course">
<column>id</column>
<column>shortname</column>
<column>fullname</column>
<row>
<value>6</value>
<value>101</value>
<value>1-0-1</value>
</row>
<row>
<value>8</value>
<value>202</value>
<value>2-0-2</value>
</row>
</table>
</dataset>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<column>toomany</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<nocolumn>id</nocolumn>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<row>
<value>tooearly</value>
</row>
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table noname="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<nodataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</nodataset>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<notable name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</notable>
</dataset>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<novalue>5</novalue>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
<table name="user">
<column>id</column>
<column>username</column>
<column>email</column>
<row>
<value>5</value>
<value>pepa.novak</value>
<value>pepa@example.com</value>
</row>
<row>
<value>7</value>
<value>bozka.novakova</value>
<value>bozka@example.com</value>
</row>
</table>
</dataset>
+924
View File
@@ -0,0 +1,924 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test phpunit_dataset features.
*
* @package core
* @category tests
* @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);
namespace core;
use advanced_testcase;
use phpunit_dataset;
use org\bovigo\vfs\vfsStream;
/**
* Test phpunit_dataset features.
*
* @coversDefaultClass \phpunit_dataset
*/
class phpunit_dataset_test extends advanced_testcase {
/**
* @covers ::from_files
*/
public function test_from_files(): void {
$ds = new phpunit_dataset();
$files = [
__DIR__ . '/fixtures/sample_dataset.xml',
'user2' => __DIR__ . '/fixtures/sample_dataset.csv',
];
// We need public properties to check the basis.
$dsref = new \ReflectionClass($ds);
$dstables = $dsref->getProperty('tables');
$dscolumns = $dsref->getProperty('columns');
$dsrows = $dsref->getProperty('rows');
// Expectations.
$exptables = ['user', 'user2'];
$expcolumns = ['id', 'username', 'email'];
$exprows = [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
];
$ds->from_files($files);
$this->assertIsArray($dstables->getValue($ds));
$this->assertSame($exptables, $dstables->getValue($ds));
$this->assertIsArray($dscolumns->getValue($ds));
$this->assertSame($expcolumns, $dscolumns->getValue($ds)['user']);
$this->assertSame($expcolumns, $dscolumns->getValue($ds)['user2']);
$this->assertIsArray($dsrows->getValue($ds));
$this->assertEquals($exprows, $dsrows->getValue($ds)['user']); // Equals because of stringified integers on load.
$this->assertEquals($exprows, $dsrows->getValue($ds)['user2']); // Equals because of stringified integers on load.
}
/**
* test_from_file() data provider.
*/
public function from_file_provider() {
// Create an unreadable file with vfsStream.
$vfsfile = vfsStream::newFile('unreadable', 0222);
vfsStream::setup('root')->addChild($vfsfile);
return [
'file not found' => [
'fullpath' => '/this/does/not/exist',
'tablename' => 'user',
'exception' => 'from_file, file not found: /this/does/not/exist',
'tables' => [],
'columns' => [],
'rows' => [],
],
'file not readable' => [
'fullpath' => $vfsfile->url(),
'tablename' => 'user',
'exception' => 'from_file, file not readable: ' . $vfsfile->url(),
'tables' => [],
'columns' => [],
'rows' => [],
],
'wrong extension' => [
'fullpath' => __DIR__ . '/fixtures/sample_dataset.txt',
'tablename' => 'user',
'exception' => 'from_file, cannot handle files with extension: txt',
'tables' => [],
'columns' => [],
'rows' => [],
],
'csv loads ok' => [
'fullpath' => __DIR__ . '/fixtures/sample_dataset.csv',
'tablename' => 'user',
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'username', 'email']
],
'rows' => ['user' =>
[
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
'xml loads ok' => [
'fullpath' => __DIR__ . '/fixtures/sample_dataset.xml',
'tablename' => 'user',
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'username', 'email']
],
'rows' => ['user' =>
[
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
];
}
/**
* @dataProvider from_file_provider
* @covers ::from_file
*/
public function test_from_file(string $fullpath, string $tablename, ?string $exception,
array $tables, array $columns, array $rows): void {
$ds = new phpunit_dataset();
// We need public properties to check the basis.
$dsref = new \ReflectionClass($ds);
$dstables = $dsref->getProperty('tables');
$dscolumns = $dsref->getProperty('columns');
$dsrows = $dsref->getProperty('rows');
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_file($fullpath, $tablename);
$this->assertIsArray($dstables->getValue($ds));
$this->assertSame($tables, $dstables->getValue($ds));
$this->assertIsArray($dscolumns->getValue($ds));
$this->assertSame($columns, $dscolumns->getValue($ds));
$this->assertIsArray($dsrows->getValue($ds));
$this->assertEquals($rows, $dsrows->getValue($ds)); // Equals because of stringified integers on load.
}
/**
* test_from_string() data provider.
*/
public function from_string_provider() {
return [
'wrong type' => [
'content' => file_get_contents(__DIR__ . '/fixtures/sample_dataset.xml'),
'type' => 'txt',
'tablename' => 'user',
'exception' => 'from_string, cannot handle contents of type: txt',
'tables' => [],
'columns' => [],
'rows' => [],
],
'missing cvs table' => [
'content' => file_get_contents(__DIR__ . '/fixtures/sample_dataset.csv'),
'type' => 'csv',
'tablename' => '',
'exception' => 'from_string, contents of type "cvs" require a $table to be passed, none found',
'tables' => [],
'columns' => [],
'rows' => [],
],
'csv loads ok' => [
'fullpath' => file_get_contents(__DIR__ . '/fixtures/sample_dataset.csv'),
'type' => 'csv',
'tablename' => 'user',
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'username', 'email']
],
'rows' => ['user' =>
[
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
'xml loads ok' => [
'fullpath' => file_get_contents(__DIR__ . '/fixtures/sample_dataset.xml'),
'type' => 'xml',
'tablename' => 'user',
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'username', 'email']
],
'rows' => ['user' =>
[
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
];
}
/**
* @dataProvider from_string_provider
* @covers ::from_string
*/
public function test_from_string(string $content, string $type, string $tablename, ?string $exception,
array $tables, array $columns, array $rows): void {
$ds = new phpunit_dataset();
// We need public properties to check the basis.
$dsref = new \ReflectionClass($ds);
$dstables = $dsref->getProperty('tables');
$dscolumns = $dsref->getProperty('columns');
$dsrows = $dsref->getProperty('rows');
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_string($content, $type, $tablename);
$this->assertIsArray($dstables->getValue($ds));
$this->assertSame($tables, $dstables->getValue($ds));
$this->assertIsArray($dscolumns->getValue($ds));
$this->assertSame($columns, $dscolumns->getValue($ds));
$this->assertIsArray($dsrows->getValue($ds));
$this->assertEquals($rows, $dsrows->getValue($ds)); // Equals because of stringified integers on load.
}
/**
* test_from_array() data provider.
*/
public function from_array_provider() {
return [
'repeated array table many structures' => [
'structure' => [
'user' => [
['id' => 5, 'name' => 'John'],
['id' => 6, 'name' => 'Jane'],
],
],
'exception' => 'from_array, table already added to dataset: user',
'tables' => [],
'columns' => [],
'rows' => [],
'repeated' => true, // To force the table already exists exception.
],
'wrong number of columns' => [
'structure' => [
'user' => [
['id' => 5, 'name' => 'John'],
['id' => 6],
],
],
'exception' => 'from_array, number of columns must match number of values, found: 2 vs 1',
'tables' => [],
'columns' => [],
'rows' => [],
],
'wrong not matching names of columns' => [
'structure' => [
'user' => [
['id' => 5, 'name' => 'John'],
['id' => 6, 'noname' => 'Jane'],
],
],
'exception' => 'from_array, columns in all elements must match first one, found: id, noname',
'tables' => [],
'columns' => [],
'rows' => [],
],
'ok non-associative format' => [
'structure' => [
'user' => [
['id', 'name'],
[5, 'John'],
[6, 'Jane'],
],
],
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'name'],
],
'rows' => ['user' =>
[
['id' => 5, 'name' => 'John'],
['id' => 6, 'name' => 'Jane'],
],
],
],
'ok associative format' => [
'structure' => [
'user' => [
['id' => 5, 'name' => 'John'],
['id' => 6, 'name' => 'Jane'],
],
],
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'name'],
],
'rows' => ['user' =>
[
['id' => 5, 'name' => 'John'],
['id' => 6, 'name' => 'Jane'],
],
],
],
'ok multiple' => [
'structure' => [
'user' => [
['id' => 5, 'name' => 'John'],
['id' => 6, 'name' => 'Jane'],
],
'course' => [
['id' => 7, 'name' => '101'],
['id' => 8, 'name' => '102'],
],
],
'exception' => null,
'tables' => ['user', 'course'],
'columns' => [
'user' => ['id', 'name'],
'course' => ['id', 'name'],
],
'rows' => [
'user' => [
['id' => 5, 'name' => 'John'],
['id' => 6, 'name' => 'Jane'],
],
'course' => [
['id' => 7, 'name' => '101'],
['id' => 8, 'name' => '102'],
],
],
],
];
}
/**
* @dataProvider from_array_provider
* @covers ::from_array
*/
public function test_from_array(array $structure, ?string $exception,
array $tables, array $columns, array $rows, ?bool $repeated = false): void {
$ds = new phpunit_dataset();
// We need public properties to check the basis.
$dsref = new \ReflectionClass($ds);
$dstables = $dsref->getProperty('tables');
$dscolumns = $dsref->getProperty('columns');
$dsrows = $dsref->getProperty('rows');
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_array($structure);
if ($repeated) {
$ds->from_array($structure);
}
$this->assertIsArray($dstables->getValue($ds));
$this->assertSame($tables, $dstables->getValue($ds));
$this->assertIsArray($dscolumns->getValue($ds));
$this->assertSame($columns, $dscolumns->getValue($ds));
$this->assertIsArray($dsrows->getValue($ds));
$this->assertEquals($rows, $dsrows->getValue($ds)); // Equals because of stringified integers on load.
}
/**
* test_load_csv() data provider.
*/
public function load_csv_provider() {
return [
'repeated csv table many files' => [
'files' => [
__DIR__ . '/fixtures/sample_dataset.xml',
'user' => __DIR__ . '/fixtures/sample_dataset.csv',
],
'exception' => 'csv_dataset_format, table already added to dataset: user',
'tables' => [],
'columns' => [],
'rows' => [],
],
'ok one csv file' => [
'files' => [
'user' => __DIR__ . '/fixtures/sample_dataset.csv',
],
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'username', 'email']
],
'rows' => ['user' =>
[
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
'ok multiple csv files' => [
'files' => [
'user1' => __DIR__ . '/fixtures/sample_dataset.csv',
'user2' => __DIR__ . '/fixtures/sample_dataset.csv',
],
'exception' => null,
'tables' => ['user1', 'user2'],
'columns' => [
'user1' => ['id', 'username', 'email'],
'user2' => ['id', 'username', 'email'],
],
'rows' => [
'user1' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'user2' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
],
],
];
}
/**
* @dataProvider load_csv_provider
* @covers ::load_csv
*/
public function test_load_csv(array $files, ?string $exception,
array $tables, array $columns, array $rows): void {
$ds = new phpunit_dataset();
// We need public properties to check the basis.
$dsref = new \ReflectionClass($ds);
$dstables = $dsref->getProperty('tables');
$dscolumns = $dsref->getProperty('columns');
$dsrows = $dsref->getProperty('rows');
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_files($files);
$this->assertIsArray($dstables->getValue($ds));
$this->assertSame($tables, $dstables->getValue($ds));
$this->assertIsArray($dscolumns->getValue($ds));
$this->assertSame($columns, $dscolumns->getValue($ds));
$this->assertIsArray($dsrows->getValue($ds));
$this->assertEquals($rows, $dsrows->getValue($ds)); // Equals because of stringified integers on load.
}
/**
* test_load_xml() data provider.
*/
public function load_xml_provider() {
return [
'repeated xml table multiple files' => [
'files' => [
'user' => __DIR__ . '/fixtures/sample_dataset.csv',
__DIR__ . '/fixtures/sample_dataset.xml',
],
'exception' => 'xml_dataset_format, table already added to dataset: user',
'tables' => [],
'columns' => [],
'rows' => [],
],
'repeated xml table one file' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_repeated.xml'],
'exception' => 'xml_dataset_format, table already added to dataset: user',
'tables' => [],
'columns' => [],
'rows' => [],
],
'wrong dataset element' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_wrong_dataset.xml'],
'exception' => 'xml_dataset_format, main xml element must be "dataset", found: nodataset',
'tables' => [],
'columns' => [],
'rows' => [],
],
'wrong table element' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_wrong_table.xml'],
'exception' => 'xml_dataset_format, only "table" elements allowed, found: notable',
'tables' => [],
'columns' => [],
'rows' => [],
],
'wrong table name attribute' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_wrong_attribute.xml'],
'exception' => 'xml_dataset_format, "table" element only allows "name" attribute',
'tables' => [],
'columns' => [],
'rows' => [],
],
'only col and row allowed' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_only_colrow.xml'],
'exception' => 'xml_dataset_format, only "column or "row" elements allowed, found: nocolumn',
'tables' => [],
'columns' => [],
'rows' => [],
],
'wrong value element' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_wrong_value.xml'],
'exception' => 'xml_dataset_format, only "value" elements allowed, found: novalue',
'tables' => [],
'columns' => [],
'rows' => [],
],
'column before row' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_col_before_row.xml'],
'exception' => 'xml_dataset_format, "column" elements always must be before "row" ones',
'tables' => [],
'columns' => [],
'rows' => [],
],
'row after column' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_row_after_col.xml'],
'exception' => 'xml_dataset_format, "row" elements always must be after "column" ones',
'tables' => [],
'columns' => [],
'rows' => [],
],
'number of columns' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_number_of_columns.xml'],
'exception' => 'xml_dataset_format, number of columns must match number of values, found: 4 vs 3',
'tables' => [],
'columns' => [],
'rows' => [],
],
'ok one xml file' => [
'files' => [__DIR__ . '/fixtures/sample_dataset.xml'],
'exception' => null,
'tables' => ['user'],
'columns' => ['user' =>
['id', 'username', 'email']
],
'rows' => ['user' =>
[
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
'ok multiple xml files' => [
'files' => [
'user1' => __DIR__ . '/fixtures/sample_dataset.csv',
__DIR__ . '/fixtures/sample_dataset.xml',
],
'exception' => null,
'tables' => ['user1', 'user'],
'columns' => [
'user1' => ['id', 'username', 'email'],
'user' => ['id', 'username', 'email'],
],
'rows' => [
'user1' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'user' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
],
],
'ok many tables in one xml' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many.xml'],
'exception' => null,
'tables' => ['user', 'course'],
'columns' => [
'user' => ['id', 'username', 'email'],
'course' => ['id', 'shortname', 'fullname'],
],
'rows' => [
'user' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'course' => [
['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
];
}
/**
* @dataProvider load_xml_provider
* @covers ::load_xml
*/
public function test_load_xml(array $files, ?string $exception,
array $tables, array $columns, array $rows): void {
$ds = new phpunit_dataset();
// We need public properties to check the basis.
$dsref = new \ReflectionClass($ds);
$dstables = $dsref->getProperty('tables');
$dscolumns = $dsref->getProperty('columns');
$dsrows = $dsref->getProperty('rows');
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_files($files);
$this->assertIsArray($dstables->getValue($ds));
$this->assertSame($tables, $dstables->getValue($ds));
$this->assertIsArray($dscolumns->getValue($ds));
$this->assertSame($columns, $dscolumns->getValue($ds));
$this->assertIsArray($dsrows->getValue($ds));
$this->assertEquals($rows, $dsrows->getValue($ds)); // Equals because of stringified integers on load.
}
/**
* test_to_database() data provider.
*/
public function to_database_provider() {
return [
'wrong table requested' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_insert.xml'],
'filter' => ['wrongtable'],
'exception' => 'dataset_to_database, table is not in the dataset: wrongtable',
'columns' => [],
'rows' => [],
],
'one table insert' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_insert.xml'],
'filter' => [],
'exception' => null,
'columns' => [
'user' => ['username', 'email'],
],
'rows' => ['user' =>
[
(object)['username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
(object)['username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
'one table import' => [
'files' => [__DIR__ . '/fixtures/sample_dataset.xml'],
'filter' => [],
'exception' => null,
'columns' => [
'user' => ['id', 'username', 'email'],
],
'rows' => ['user' =>
[
(object)['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
(object)['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
]
],
],
'multiple table many files import' => [
'files' => [
__DIR__ . '/fixtures/sample_dataset.xml',
__DIR__ . '/fixtures/sample_dataset2.xml',
],
'filter' => [],
'exception' => null,
'columns' => [
'user' => ['id', 'username', 'email'],
'course' => ['id', 'shortname', 'fullname'],
],
'rows' => [
'user' => [
(object)['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
(object)['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'course' => [
(object)['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
(object)['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
'multiple table one file import' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many.xml'],
'filter' => [],
'exception' => null,
'columns' => [
'user' => ['id', 'username', 'email'],
'course' => ['id', 'shortname', 'fullname'],
],
'rows' => [
'user' => [
(object)['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
(object)['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'course' => [
(object)['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
(object)['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
'filtering tables' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many.xml'],
'filter' => ['course'],
'exception' => null,
'columns' => [
'user' => ['id', 'username', 'email'],
'course' => ['id', 'shortname', 'fullname'],
],
'rows' => [
'user' => [], // Table user is being excluded via filter, expect no rows sent to database.
'course' => [
(object)['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
(object)['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
];
}
/**
* @dataProvider to_database_provider
* @covers ::to_database
*/
public function test_to_database(array $files, ?array $filter, ?string $exception, array $columns, array $rows): void {
global $DB;
$this->resetAfterTest();
// Grab the status before loading to database.
$before = [];
foreach ($columns as $tablename => $tablecolumns) {
if (!isset($before[$tablename])) {
$before[$tablename] = [];
}
$before[$tablename] = $DB->get_records($tablename, null, '', implode(', ', $tablecolumns));
}
$ds = new phpunit_dataset();
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_files($files);
$ds->to_database($filter);
// Grab the status after loading to database.
$after = [];
foreach ($columns as $tablename => $tablecolumns) {
if (!isset($after[$tablename])) {
$after[$tablename] = [];
}
$sortandcol = implode(', ', $tablecolumns);
$after[$tablename] = $DB->get_records($tablename, null, $sortandcol, $sortandcol);
}
// Differences must match the expectations.
foreach ($rows as $tablename => $expectedrows) {
$changes = array_udiff($after[$tablename], $before[$tablename], function ($b, $a) {
if ((array)$b > (array)$a) {
return 1;
} else if ((array)$b < (array)$a) {
return -1;
} else {
return 0;
}
});
$this->assertEquals(array_values($expectedrows), array_values($changes));
}
}
/**
* test_get_rows() data provider.
*/
public function get_rows_provider() {
return [
'wrong table requested' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many.xml'],
'filter' => ['wrongtable'],
'exception' => 'dataset_get_rows, table is not in the dataset: wrongtable',
'rows' => [],
],
'ok get rows from empty tables' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many_with_empty.xml'],
'filter' => ['empty1', 'empty2'],
'exception' => null,
'rows' => [
'empty1' => [],
'empty2' => [],
],
],
'ok get rows from one table' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many_with_empty.xml'],
'filter' => ['user'],
'exception' => null,
'rows' => [
'user' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
],
],
'ok get rows from two tables' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many_with_empty.xml'],
'filter' => ['user', 'course'],
'exception' => null,
'rows' => [
'user' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'course' => [
['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
'ok get rows from three tables' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many_with_empty.xml'],
'filter' => ['user', 'empty1', 'course'],
'exception' => null,
'rows' => [
'user' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'empty1' => [],
'course' => [
['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
'ok no filter returns all' => [
'files' => [__DIR__ . '/fixtures/sample_dataset_many_with_empty.xml'],
'filter' => [],
'exception' => null,
'rows' => [
'user' => [
['id' => 5, 'username' => 'bozka.novakova', 'email' => 'bozka@example.com'],
['id' => 7, 'username' => 'pepa.novak', 'email' => 'pepa@example.com'],
],
'empty1' => [],
'empty2' => [],
'course' => [
['id' => 6, 'shortname' => '101', 'fullname' => '1-0-1'],
['id' => 8, 'shortname' => '202', 'fullname' => '2-0-2'],
],
],
],
];
}
/**
* @dataProvider get_rows_provider
* @covers ::get_rows
*/
public function test_get_rows(array $files, array $filter, ?string $exception, array $rows): void {
$ds = new phpunit_dataset();
// We are expecting an exception.
if (!empty($exception)) {
$this->expectException('coding_exception');
$this->expectExceptionMessage($exception);
}
$ds->from_files($files);
$this->assertEquals($rows, $ds->get_rows($filter));
}
}
+97
View File
@@ -0,0 +1,97 @@
<?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 util extra features.
*
* @package core
* @category test
* @copyright 2015 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class util_test extends \advanced_testcase {
/**
* @dataProvider set_table_modified_by_sql_provider
*/
public function test_set_table_modified_by_sql($sql, $expectations): void {
\phpunit_util::reset_updated_table_list();
\phpunit_util::set_table_modified_by_sql($sql);
foreach ($expectations as $table => $present) {
$this->assertEquals($present, !empty(\phpunit_util::$tableupdated[$table]));
}
}
public function set_table_modified_by_sql_provider() {
global $DB;
$prefix = $DB->get_prefix();
return array(
'Basic update' => array(
'sql' => "UPDATE {$prefix}user SET username = username || '_test'",
'expectations' => array(
'user' => true,
'course' => false,
),
),
'Basic update with a fieldname sharing the same prefix' => array(
'sql' => "UPDATE {$prefix}user SET {$prefix}username = username || '_test'",
'expectations' => array(
'user' => true,
'course' => false,
),
),
'Basic update with a table which contains the prefix' => array(
'sql' => "UPDATE {$prefix}user{$prefix} SET username = username || '_test'",
'expectations' => array(
"user{$prefix}" => true,
'course' => false,
),
),
'Update table with a numeric name' => array(
'sql' => "UPDATE {$prefix}example42 SET username = username || '_test'",
'expectations' => array(
'example42' => true,
'user' => false,
'course' => false,
),
),
'Drop basic table' => array(
'sql' => "DROP TABLE {$prefix}user",
'expectations' => array(
'user' => true,
'course' => false,
),
),
'Drop table with a numeric name' => array(
'sql' => "DROP TABLE {$prefix}example42",
'expectations' => array(
'example42' => true,
'user' => false,
'course' => false,
),
),
'Insert in table' => array(
'sql' => "INSERT INTO {$prefix}user (username,password) VALUES ('moodle', 'test')",
'expectations' => array(
'user' => true,
'course' => false,
),
),
);
}
}