ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç privacy_helper.php000064400000012106152266505450010303 0ustar00. /** * Helpers for the core_rating subsystem implementation of privacy. * * @package core_rating * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_rating\phpunit; defined('MOODLE_INTERNAL') || die(); use \core_privacy\tests\request\content_writer; global $CFG; /** * Helpers for the core_rating subsystem implementation of privacy. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait privacy_helper { /** * Fetch all ratings on a subcontext. * * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @return \stdClass|array */ protected function get_ratings_on_subcontext(\context $context, array $subcontext) { $writer = \core_privacy\local\request\writer::with_context($context); return $writer->get_related_data($subcontext, 'rating'); } /** * Check that all included ratings belong to the specified user. * * @param int $userid The ID of the user being stored. * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @param string $component The component being stored. * @param string $ratingarea The rating area to store results for. * @param int $itemid The itemid to store. */ protected function assert_all_own_ratings_on_context( int $userid, \context $context, array $subcontext, $component, $ratingarea, $itemid ) { $writer = \core_privacy\local\request\writer::with_context($context); $rm = new \rating_manager(); $dbratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, ]); $exportedratings = $this->get_ratings_on_subcontext($context, $subcontext); foreach ($exportedratings as $ratingid => $rating) { $this->assertTrue(isset($dbratings[$ratingid])); $this->assertEquals($userid, $rating->author); $this->assert_rating_matches($dbratings[$ratingid], $rating); } foreach ($dbratings as $rating) { if ($rating->userid == $userid) { $this->assertEquals($rating->id, $ratingid); } } } /** * Check that all included ratings are valid. They may belong to any user. * * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @param string $component The component being stored. * @param string $ratingarea The rating area to store results for. * @param int $itemid The itemid to store. */ protected function assert_all_ratings_on_context(\context $context, array $subcontext, $component, $ratingarea, $itemid) { $writer = \core_privacy\local\request\writer::with_context($context); $rm = new \rating_manager(); $dbratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, ]); $exportedratings = $this->get_ratings_on_subcontext($context, $subcontext); foreach ($exportedratings as $ratingid => $rating) { $this->assertTrue(isset($dbratings[$ratingid])); $this->assert_rating_matches($dbratings[$ratingid], $rating); } foreach ($dbratings as $rating) { $this->assertTrue(isset($exportedratings[$rating->id])); } } /** * Assert that the rating matches. * * @param \stdClass $expected The expected rating structure * @param \stdClass $stored The actual rating structure */ protected function assert_rating_matches($expected, $stored) { $this->assertEquals($expected->rating, $stored->rating); $this->assertEquals($expected->userid, $stored->author); } } lib.php000064400000003263152267207670006045 0ustar00. /** * 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'); bootstrap.php000064400000024075152267207670007320 0ustar00. /** * 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); 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); 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); // 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(); classes/database_driver_testcase.php000064400000016716152267207670013755 0ustar00. /** * 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_'), * ); * 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 */ final public function __construct($name = null) { parent::__construct($name); $this->setBackupGlobals(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; } #[Before] protected function setup_extradb(): void { global $DB; if (self::$extradb) { $this->tdb = self::$extradb; } else { $this->tdb = $DB; } } #[After] protected function teardown_extradb(): 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); } } } public static function tearDownAfterClass(): void { if (self::$extradb) { self::$extradb->dispose(); self::$extradb = null; } phpunit_util::reset_all_data(null); parent::tearDownAfterClass(); } #[After] public function check_debugging(): void { // 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); } } /** * 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); } } classes/base_testcase.php000064400000056442152267207670011550 0ustar00. /** * Base test case class. * * @package core * @category test * @author Tony Levi * @copyright 2015 Blackboard (http://www.blackboard.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use PHPUnit\Framework\MockObject\Rule\InvocationOrder; use PHPUnit\Framework\TestCase; /** * 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 * @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) { if ($ishtml) { $dom = self::loadHTML($actual); } else { $dom = (new PHPUnit\Util\Xml\Loader)->load($actual); } $tags = self::findNodes($dom, $matcher, $ishtml); $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode; self::assertTrue($matched, $message); } /** * Load HTML into a DomDocument. * * Note: THis is a replacement for functionality removed from PHPUnit 10. * * @param string $actual * @throws \PHPUnit\Util\Xml\XmlException * @return DOMDocument */ public static function loadHTML(string $actual): DOMDocument { if ($actual === '') { throw new \PHPUnit\Util\Xml\XmlException('Could not load XML from empty string'); } $document = new DOMDocument; $document->preserveWhiteSpace = false; $internal = libxml_use_internal_errors(true); $message = ''; $reporting = error_reporting(0); $loaded = $document->loadHTML($actual); foreach (libxml_get_errors() as $error) { $message .= "\n" . $error->message; } libxml_use_internal_errors($internal); error_reporting($reporting); if ($loaded === false) { if ($message === '') { $message = 'Could not load XML for unknown reason'; } throw new \PHPUnit\Util\Xml\XmlException($message); } return $document; } /** * 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) { if ($ishtml) { $dom = self::loadHTML($actual); } else { $dom = (new PHPUnit\Util\Xml\Loader)->load($actual); } $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); } /** * Helper to get the count of invocation. * * This is required because the method to use changed names in PHPUnit 10.0 in a breaking change. * * @param \PHPUnit\Framework\MockObject\Rule\InvocationOrder $counter * @return int */ protected static function getInvocationCount(InvocationOrder $counter): int { if (method_exists($counter, 'numberOfInvocations')) { return $counter->numberOfInvocations(); } return $counter->getInvocationCount(); } // phpcs:enable /** * Get an invokable object for testing. * * This is a helper method to create an invokable object for testing which can be used to * track invocations, including arguments provided. * * This can be useful for modifications to the error handler. * * @return object */ protected static function get_invokable() { return new class { private array $invocations = []; public function __invoke(...$args) { $this->invocations[] = $args; } public function get_invocations(): array { return $this->invocations; } public function get_invocation_count(): int { return count($this->invocations); } public function reset(): void { $this->invocations = []; } }; } /** * Determine whether the test is running in isolation. * * Note: This was previously a public method of the TestCase, but as removed in PHPUnit 10. * There is no direct replacement, but we can use reflection to access the protected property. * @return bool */ public function isInIsolation(): bool { $rc = new \ReflectionClass(TestCase::class); $rcp = $rc->getProperty('inIsolation'); return $rcp->getValue($this); } } classes/exception/test_exception.php000064400000002054152267207670013764 0ustar00. namespace core_phpunit\exception; /** * An exception thrown when an error is encountered in the setup, teardown, or other non-test parts of a unit test. * * @package core_testing * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_exception extends \core\exception\coding_exception { } classes/basic_testcase.php000064400000003777152267207670011722 0ustar00. /** * 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. * * * @param string $name */ final public function __construct($name = null) { parent::__construct($name); $this->setBackupGlobals(false); $this->setRunTestInSeparateProcess(false); } /** * Teardown method to reset data after tests. */ #[After] final public function teardown_after_test(): void { global $DB; 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); } /** * Get the name of the test. * * Replaces the original PHPUnit method. * @return string */ final public function getName(): string { return $this->name(); } } classes/restore_date_testcase.php000064400000012507152267207670013310 0ustar00. /** * 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 = []) { // 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); } } classes/advanced_testcase.php000064400000073464152267207670012406 0ustar00. use core\di; use core\hook; use core\http_client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; /** * 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 */ final public function __construct(string $name) { parent::__construct($name); $this->setBackupGlobals(false); $this->setPreserveGlobalState(false); } #[Before] final public function setup_test_environment(): 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(); } $this->setCurrentTimeStart(); } #[After] final public function teardown_test_environment(): void { global $CFG, $DB; // 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(); // 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'); } } } /** * 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 static 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 static 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 static 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 static function getExternalTestFileUrl( string $path, bool $https = false, ): string { $path = ltrim($path, '/'); if ($path) { $path = "/{$path}"; } if ($https) { if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) { if (!TEST_EXTERNAL_FILES_HTTPS_URL) { self::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) { self::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->createStub(\core\lock\lock::class); $cronlock = $this->createStub(\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 * @param bool $deprecated whether to add the plugintype as a deprecated plugin type. */ protected function add_mocked_plugintype( string $plugintype, string $path, bool $deprecated = false, ): void { require_phpunit_isolation(); $mockedcomponent = new \ReflectionClass(\core_component::class); $propertyname = $deprecated ? 'deprecatedplugintypes' : 'plugintypes'; $plugintypes = $mockedcomponent->getStaticPropertyValue($propertyname); if (array_key_exists($plugintype, $plugintypes)) { throw new \coding_exception("The plugintype '{$plugintype}' already exists."); } $plugintypes[$plugintype] = $path; $mockedcomponent->setStaticPropertyValue($propertyname, $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 get the path to a fixture. * * @param string $component * @param string $path * @throws coding_exception */ protected static function get_fixture_path( string $component, string $path, ): string { return sprintf( "%s/tests/fixtures/%s", \core_component::get_component_directory($component), $path, ); } /** * 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 { global $ADMIN; global $CFG; global $DB; global $SITE; global $USER; global $OUTPUT; global $PAGE; global $SESSION; global $COURSE; global $SITE; $fullpath = static::get_fixture_path($component, $path); if (!file_exists($fullpath)) { throw new \coding_exception("Fixture file not found: $fullpath"); } require_once($fullpath); } /** * Get a mocked HTTP Client, inserting it into the Dependency Injector. * * @param array|null $history An array which will contain the Request/Response history of the HTTP client * @return array Containing the client, the mock, and the history */ protected function get_mocked_http_client( ?array &$history = null, ): array { $mock = new MockHandler([]); $handlerstack = HandlerStack::create($mock); if ($history !== null) { $historymiddleware = Middleware::history($history); $handlerstack->push($historymiddleware); } $client = new http_client(['handler' => $handlerstack]); di::set(http_client::class, $client); return [ 'client' => $client, 'mock' => $mock, 'handlerstack' => $handlerstack, ]; } /** * Get a copy of the mocked string manager. * * @return \core\tests\mocking_string_manager */ protected function get_mocked_string_manager(): \core\tests\mocking_string_manager { global $CFG; $this->resetAfterTest(); $CFG->config_php_settings['customstringmanager'] = \core\tests\mocking_string_manager::class; return get_string_manager(true); } } classes/constraint_object_is_equal_with_exceptions.php000064400000010137152267207670017622 0ustar00. /** * 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 * @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 * @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(); } } classes/phpmailer_sink.php000064400000004404152267207670011737 0ustar00. /** * 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(); } } classes/event_sink.php000064400000004434152267207670011102 0ustar00. /** * 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(); } } classes/message_sink.php000064400000006630152267207670011405 0ustar00. /** * 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(); } } classes/util.php000064400000110766152267207670007720 0ustar00. /** * Utility class. * * @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 */ use core\di; use core\hook; require_once(__DIR__.'/../../testing/classes/util.php'); require_once(__DIR__ . "/coverage_info.php"); /** * Collection of utility methods. * * @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_util extends testing_util { /** * @var int last value of db writes counter, used for db resetting */ public static $lastdbwrites = null; /** @var array An array of original globals, restored after each test */ protected static $globals = array(); /** @var array list of debugging messages triggered during the last test execution */ protected static $debuggings = array(); /** @var phpunit_message_sink alternative target for moodle messaging */ protected static $messagesink = null; /** @var phpunit_phpmailer_sink alternative target for phpmailer messaging */ protected static $phpmailersink = null; /** @var phpunit_message_sink alternative target for moodle messaging */ protected static $eventsink = null; /** * @var array Files to skip when resetting dataroot folder */ protected static $datarootskiponreset = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess'); /** * @var array Files to skip when dropping dataroot folder */ protected static $datarootskipondrop = array('.', '..', 'lock'); /** * Load global $CFG; * @internal * @static * @return void */ public static function initialise_cfg() { global $DB; $dbhash = false; try { $dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest')); } catch (Exception $e) { // not installed yet initialise_cfg(); return; } if ($dbhash !== core_component::get_all_versions_hash()) { // do not set CFG - the only way forward is to drop and reinstall return; } // standard CFG init initialise_cfg(); } /** * Reset contents of all database tables to initial values, reset caches, etc. * * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! * * @static * @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 reset_all_data($detectchanges = false) { global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME, $FILTERLIB_PRIVATE; // Stop all hook redirections. di::get(hook\manager::class)->phpunit_stop_redirections(); // Stop any message redirection. self::stop_message_redirection(); // Stop any message redirection. self::stop_event_redirection(); // Start a new email redirection. // This will clear any existing phpmailer redirection. // We redirect all phpmailer output to this message sink which is // called instead of phpmailer actually sending the message. self::start_phpmailer_redirection(); // We used to call gc_collect_cycles here to ensure desctructors were called between tests. // This accounted for 25% of the total time running phpunit - so we removed it. // Show any unhandled debugging messages, the runbare() could already reset it. self::display_debugging_messages(); self::reset_debugging(); // reset global $DB in case somebody mocked it $DB = self::get_global_backup('DB'); if ($DB->is_transaction_started()) { // we can not reset inside transaction $DB->force_transaction_rollback(); } $resetdb = self::reset_database(); $localename = self::get_locale_name(); $warnings = array(); if ($detectchanges === true) { if ($resetdb) { $warnings[] = 'Warning: unexpected database modification, resetting DB state'; } $oldcfg = self::get_global_backup('CFG'); $oldsite = self::get_global_backup('SITE'); foreach($CFG as $k=>$v) { if (!property_exists($oldcfg, $k)) { $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value'; } else if ($oldcfg->$k !== $CFG->$k) { $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value'; } unset($oldcfg->$k); } if ($oldcfg) { foreach($oldcfg as $k=>$v) { $warnings[] = 'Warning: unexpected removal of $CFG->'.$k; } } if ($USER->id != 0) { $warnings[] = 'Warning: unexpected change of $USER'; } if ($COURSE->id != $oldsite->id) { $warnings[] = 'Warning: unexpected change of $COURSE'; } if ($FULLME !== self::get_global_backup('FULLME')) { $warnings[] = 'Warning: unexpected change of $FULLME'; } if (setlocale(LC_TIME, 0) !== $localename) { $warnings[] = 'Warning: unexpected change of locale'; } } if (ini_get('max_execution_time') != 0) { // This is special warning for all resets because we do not want any // libraries to mess with timeouts unintentionally. // Our PHPUnit integration is not supposed to change it either. if ($detectchanges !== false) { $warnings[] = 'Warning: max_execution_time was changed to '.ini_get('max_execution_time'); } set_time_limit(0); } // restore original globals $_SERVER = self::get_global_backup('_SERVER'); $CFG = self::get_global_backup('CFG'); $SITE = self::get_global_backup('SITE'); $FULLME = self::get_global_backup('FULLME'); $_GET = array(); $_POST = array(); $_FILES = array(); $_REQUEST = array(); $COURSE = $SITE; // reinitialise following globals $OUTPUT = new bootstrap_renderer(); $PAGE = new moodle_page(); \navigation_node::reset_all_data(); $FULLME = null; $ME = null; $SCRIPT = null; $FILTERLIB_PRIVATE = null; if (!empty($SESSION->notifications)) { $SESSION->notifications = []; } // Empty sessison and set fresh new not-logged-in user. \core\session\manager::init_empty_session(); // reset all static caches \core\event\manager::phpunit_reset(); accesslib_clear_all_caches(true); accesslib_reset_role_cache(); get_string_manager()->reset_caches(true); reset_text_filters_cache(true); get_message_processors(false, true, true); filter_manager::reset_caches(); core_filetypes::reset_caches(); \core_search\manager::clear_static(); core_user::reset_caches(); \core\output\icon_system::reset_caches(); if (class_exists('core_media_manager', false)) { core_media_manager::reset_caches(); } // Reset static unit test options. if (class_exists('\availability_date\condition', false)) { \availability_date\condition::set_current_time_for_test(0); } // Reset internal users. core_user::reset_internal_users(); // Clear static caches in calendar container. if (class_exists('\core_calendar\local\event\container', false)) { core_calendar\local\event\container::reset_caches(); } //TODO MDL-25290: add more resets here and probably refactor them to new core function // Reset course and module caches. core_courseformat\base::reset_course_cache(0); get_fast_modinfo(0, 0, true); // Reset other singletons. if (class_exists('core_plugin_manager')) { core_plugin_manager::reset_caches(true); } if (class_exists('\core\update\checker')) { \core\update\checker::reset_caches(true); } if (class_exists('\core_course\customfield\course_handler')) { \core_course\customfield\course_handler::reset_caches(); } if (class_exists('\core_reportbuilder\manager')) { \core_reportbuilder\manager::reset_caches(); } if (class_exists('\core_cohort\customfield\cohort_handler')) { \core_cohort\customfield\cohort_handler::reset_caches(); } if (class_exists('\core_group\customfield\group_handler')) { \core_group\customfield\group_handler::reset_caches(); } if (class_exists('\core_group\customfield\grouping_handler')) { \core_group\customfield\grouping_handler::reset_caches(); } if (class_exists('\core_reportbuilder\customfield\report_handler')) { \core_reportbuilder\customfield\report_handler::reset_caches(); } // Clear static cache within restore. if (class_exists('restore_section_structure_step')) { restore_section_structure_step::reset_caches(); } // purge dataroot directory self::reset_dataroot(); // restore original config once more in case resetting of caches changed CFG $CFG = self::get_global_backup('CFG'); // inform data generator self::get_data_generator()->reset(); // fix PHP settings error_reporting($CFG->debug); // Reset the date/time class. core_date::phpunit_reset(); // Make sure the time locale is consistent - that is Australian English. setlocale(LC_TIME, $localename); // Reset the log manager cache. get_log_manager(true); // Reset user agent. core_useragent::instance(true, null); // Reset the DI container. \core\di::reset_container(); // verify db writes just in case something goes wrong in reset if (self::$lastdbwrites != $DB->perf_get_writes()) { error_log('Unexpected DB writes in phpunit_util::reset_all_data()'); self::$lastdbwrites = $DB->perf_get_writes(); } if ($warnings) { $warnings = implode("\n", $warnings); throw new \core_phpunit\exception\test_exception($warnings); } } /** * Reset all database tables to default values. * @static * @return bool true if reset done, false if skipped */ public static function reset_database() { global $DB; if (defined('PHPUNIT_ISOLATED_TEST') && PHPUNIT_ISOLATED_TEST && self::$lastdbwrites === null) { // This is an isolated test and the lastdbwrites has not yet been initialised. // Isolated test runs are reset by the test runner before the run starts. self::$lastdbwrites = $DB->perf_get_writes(); } if (!is_null(self::$lastdbwrites) && self::$lastdbwrites == $DB->perf_get_writes()) { return false; } if (!parent::reset_database()) { return false; } self::$lastdbwrites = $DB->perf_get_writes(); return true; } /** * Called during bootstrap only! * @internal * @static * @return void */ public static function bootstrap_init() { global $CFG, $SITE, $DB, $FULLME; // backup the globals self::$globals['_SERVER'] = $_SERVER; self::$globals['CFG'] = clone($CFG); self::$globals['SITE'] = clone($SITE); self::$globals['DB'] = $DB; self::$globals['FULLME'] = $FULLME; // refresh data in all tables, clear caches, etc. self::reset_all_data(); } /** * Print some Moodle related info to console. * @internal * @static * @return void */ public static function bootstrap_moodle_info() { echo self::get_site_info(); } /** * Returns original state of global variable. * @static * @param string $name * @return mixed */ public static function get_global_backup($name) { if ($name === 'DB') { // no cloning of database object, // we just need the original reference, not original state return self::$globals['DB']; } if (isset(self::$globals[$name])) { if (is_object(self::$globals[$name])) { $return = clone(self::$globals[$name]); return $return; } else { return self::$globals[$name]; } } return null; } /** * Is this site initialised to run unit tests? * * @static * @return int array errorcode=>message, 0 means ok */ public static function testing_ready_problem() { global $DB; $localename = self::get_locale_name(); if (setlocale(LC_TIME, $localename) === false) { return array(PHPUNIT_EXITCODE_CONFIGERROR, "Required locale '$localename' is not installed."); } if (!self::is_test_site()) { // dataroot was verified in bootstrap, so it must be DB return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix'); } $tables = $DB->get_tables(false); if (empty($tables)) { return array(PHPUNIT_EXITCODE_INSTALL, ''); } if (!self::is_test_data_updated()) { return array(PHPUNIT_EXITCODE_REINSTALL, ''); } return array(0, ''); } /** * Drop all test site data. * * Note: To be used from CLI scripts only. * * @static * @param bool $displayprogress if true, this method will echo progress information. * @return void may terminate execution with exit code */ public static function drop_site($displayprogress = false) { global $DB, $CFG; if (!self::is_test_site()) { phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); } // Purge dataroot if ($displayprogress) { echo "Purging dataroot:\n"; } self::reset_dataroot(); testing_initdataroot($CFG->dataroot, 'phpunit'); // Drop all tables. self::drop_database($displayprogress); // Drop dataroot. self::drop_dataroot(); } /** * Perform a fresh test site installation * * Note: To be used from CLI scripts only. * * @static * @return void may terminate execution with exit code */ public static function install_site() { global $DB, $CFG; if (!self::is_test_site()) { phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!'); } if ($DB->get_tables()) { list($errorcode, $message) = self::testing_ready_problem(); if ($errorcode) { phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised'); } else { phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised'); } } $options = array(); $options['adminpass'] = 'admin'; $options['shortname'] = 'phpunit'; $options['fullname'] = 'PHPUnit test site'; install_cli_database($options, false); // Set the admin email address. $DB->set_field('user', 'email', 'admin@example.com', array('username' => 'admin')); // Disable all logging for performance and sanity reasons. set_config('enabled_stores', '', 'tool_log'); // Remove any default blocked hosts and port restrictions, to avoid blocking tests (eg those using local files). set_config('curlsecurityblockedhosts', ''); set_config('curlsecurityallowedport', ''); // Execute all the adhoc tasks. while ($task = \core\task\manager::get_next_adhoc_task(time())) { $task->execute(); \core\task\manager::adhoc_task_complete($task); } // We need to keep the installed dataroot filedir files. // So each time we reset the dataroot before running a test, the default files are still installed. self::save_original_data_files(); // Store version hash in the database and in a file. self::store_versions_hash(); // Store database data and structure. self::store_database_state(); } /** * Builds dirroot/phpunit.xml file using defaults from /phpunit.xml.dist * @static * @return bool true means main config file created, false means only dataroot file created */ public static function build_config_file() { global $CFG; $template = << @dir@ @dir@/classes @dir@/fixtures EOF; $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); $suites = ''; $includelists = []; $excludelists = []; $subsystems = core_component::get_core_subsystems(); $subsystems['core'] = $CFG->dirroot . '/lib'; foreach ($subsystems as $subsystem => $fulldir) { if (empty($fulldir)) { continue; } if (!file_exists("{$fulldir}/tests/")) { // There are no tests - skip this directory. continue; } $dir = substr($fulldir, strlen($CFG->dirroot) + 1); if ($coverageinfo = self::get_coverage_info($fulldir)) { $includelists = array_merge($includelists, $coverageinfo->get_includelists($dir)); $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists($dir)); } } $plugintypes = core_component::get_plugin_types(); ksort($plugintypes); foreach (array_keys($plugintypes) as $type) { $plugs = core_component::get_plugin_list($type); ksort($plugs); foreach ($plugs as $plug => $plugindir) { if (!file_exists("{$plugindir}/tests/")) { // There are no tests - skip this directory. continue; } $dir = substr($plugindir, strlen($CFG->dirroot) + 1); $testdir = "{$dir}/tests"; $component = "{$type}_{$plug}"; $suite = str_replace('@component@', $component, $template); $suite = str_replace('@dir@', $testdir, $suite); $suites .= $suite; if ($coverageinfo = self::get_coverage_info($plugindir)) { $includelists = array_merge($includelists, $coverageinfo->get_includelists($dir)); $excludelists = array_merge($excludelists, $coverageinfo->get_excludelists($dir)); } } } // Start a sequence between 100000 and 199000 to ensure each call to init produces // different ids in the database. This reduces the risk that hard coded values will // end up being placed in phpunit or behat test code. $sequencestart = 100000 + mt_rand(0, 99) * 1000; $data = preg_replace('| *.*|s', trim($suites, "\n"), $data, 1); $data = str_replace( '', '', $data); $coverages = self::get_coverage_config($includelists, $excludelists); $data = preg_replace('| *|s', trim($coverages, "\n"), $data); $result = false; if (is_writable($CFG->dirroot)) { if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) { testing_fix_file_permissions("$CFG->dirroot/phpunit.xml"); } } return (bool)$result; } /** * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist * * @static * @return void, stops if can not write files */ public static function build_component_config_files() { global $CFG; $template = << tests tests/classes tests/fixtures EOT; $coveragedefault = << . tests EOT; // Start a sequence between 100000 and 199000 to ensure each call to init produces // different ids in the database. This reduces the risk that hard coded values will // end up being placed in phpunit or behat test code. $sequencestart = 100000 + mt_rand(0, 99) * 1000; // Use the upstream file as source for the distributed configurations $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); $ftemplate = preg_replace('| *', $ftemplate); // Gets all the components with tests $components = tests_finder::get_components_with_tests('phpunit'); // Create the corresponding phpunit.xml file for each component foreach ($components as $cname => $cpath) { // Calculate the component suite $ctemplate = $template; $ctemplate = str_replace('@component@', $cname, $ctemplate); $fcontents = str_replace('', $ctemplate, $ftemplate); // Check for coverage configurations. if ($coverageinfo = self::get_coverage_info($cpath)) { $coverages = self::get_coverage_config($coverageinfo->get_includelists(''), $coverageinfo->get_excludelists('')); } else { $coverages = $coveragedefault; } $fcontents = preg_replace('| *|s', trim($coverages, "\n"), $fcontents); // Apply it to the file template. $fcontents = str_replace( '', '', $fcontents); // fix link to schema $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents); // Write the file $result = false; if (is_writable($cpath)) { if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { testing_fix_file_permissions("$cpath/phpunit.xml"); } } // Problems writing file, throw error if (!$result) { phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions"); } } } /** * To be called from debugging() only. * @param string $message * @param int $level * @param string $from */ public static function debugging_triggered($message, $level, $from) { // Store only if debugging triggered from actual test, // we need normal debugging outside of tests to find problems in our phpunit integration. $backtrace = debug_backtrace(); // Only for advanced_testcase, database_driver_testcase (and descendants). Others aren't // able to manage the debugging sink, so any debugging has to be output normally and, hopefully, // PHPUnit execution will catch that unexpected output properly. $sinksupport = false; foreach ($backtrace as $bt) { if (isset($bt['object']) && is_object($bt['object']) && ( $bt['object'] instanceof advanced_testcase || $bt['object'] instanceof database_driver_testcase) ) { $sinksupport = true; break; } } if (!$sinksupport) { return false; } // Verify that we are inside a PHPUnit test (little bit redundant, because // we already have checked above that this is an advanced/database_driver // testcase, but let's keep things double safe for now). foreach ($backtrace as $bt) { if (isset($bt['object']) && is_object($bt['object']) && $bt['object'] instanceof PHPUnit\Framework\TestCase) { $debug = new stdClass(); $debug->message = $message; $debug->level = $level; $debug->from = $from; self::$debuggings[] = $debug; return true; } } return false; } /** * Resets the list of debugging messages. */ public static function reset_debugging() { self::$debuggings = array(); set_debugging(DEBUG_DEVELOPER); } /** * Returns all debugging messages triggered during test. * @return array with instances having message, level and stacktrace property. */ public static function get_debugging_messages() { return self::$debuggings; } /** * Prints out any debug messages accumulated during test execution. * * @param bool $return true to return the messages or false to print them directly. Default false. * @return bool|string false if no debug messages, true if debug triggered or string of messages */ public static function display_debugging_messages($return = false) { if (empty(self::$debuggings)) { return false; } $debugstring = ''; foreach(self::$debuggings as $debug) { $debugstring .= 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n"; } if ($return) { return $debugstring; } echo $debugstring; return true; } /** * Start message redirection. * * Note: Do not call directly from tests, * use $sink = $this->redirectMessages() instead. * * @return phpunit_message_sink */ public static function start_message_redirection() { if (self::$messagesink) { self::stop_message_redirection(); } self::$messagesink = new phpunit_message_sink(); return self::$messagesink; } /** * End message redirection. * * Note: Do not call directly from tests, * use $sink->close() instead. */ public static function stop_message_redirection() { self::$messagesink = null; } /** * Are messages redirected to some sink? * * Note: to be called from messagelib.php only! * * @return bool */ public static function is_redirecting_messages() { return !empty(self::$messagesink); } /** * To be called from messagelib.php only! * * @param stdClass $message record from messages table * @return bool true means send message, false means message "sent" to sink. */ public static function message_sent($message) { if (self::$messagesink) { self::$messagesink->add_message($message); } } /** * Start phpmailer redirection. * * Note: Do not call directly from tests, * use $sink = $this->redirectEmails() instead. * * @return phpunit_phpmailer_sink */ public static function start_phpmailer_redirection() { if (self::$phpmailersink) { // If an existing mailer sink is active, just clear it. self::$phpmailersink->clear(); } else { self::$phpmailersink = new phpunit_phpmailer_sink(); } return self::$phpmailersink; } /** * End phpmailer redirection. * * Note: Do not call directly from tests, * use $sink->close() instead. */ public static function stop_phpmailer_redirection() { self::$phpmailersink = null; } /** * Are messages for phpmailer redirected to some sink? * * Note: to be called from moodle_phpmailer.php only! * * @return bool */ public static function is_redirecting_phpmailer() { return !empty(self::$phpmailersink); } /** * To be called from messagelib.php only! * * @param stdClass $message record from messages table * @return bool true means send message, false means message "sent" to sink. */ public static function phpmailer_sent($message) { if (self::$phpmailersink) { self::$phpmailersink->add_message($message); } } /** * Start event redirection. * * @private * Note: Do not call directly from tests, * use $sink = $this->redirectEvents() instead. * * @return phpunit_event_sink */ public static function start_event_redirection() { if (self::$eventsink) { self::stop_event_redirection(); } self::$eventsink = new phpunit_event_sink(); return self::$eventsink; } /** * End event redirection. * * @private * Note: Do not call directly from tests, * use $sink->close() instead. */ public static function stop_event_redirection() { self::$eventsink = null; } /** * Are events redirected to some sink? * * Note: to be called from \core\event\base only! * * @private * @return bool */ public static function is_redirecting_events() { return !empty(self::$eventsink); } /** * To be called from \core\event\base only! * * @private * @param \core\event\base $event record from event_read table * @return bool true means send event, false means event "sent" to sink. */ public static function event_triggered(\core\event\base $event) { if (self::$eventsink) { self::$eventsink->add_event($event); } } /** * Gets the name of the locale for testing environment (Australian English) * depending on platform environment. * * @return string the locale name. */ protected static function get_locale_name() { global $CFG; if ($CFG->ostype === 'WINDOWS') { return 'English_Australia.1252'; } else { return 'en_AU.UTF-8'; } } /** * Executes all adhoc tasks in the queue. Useful for testing asynchronous behaviour. * * @return void */ public static function run_all_adhoc_tasks() { $now = time(); while (($task = \core\task\manager::get_next_adhoc_task($now)) !== null) { try { $task->execute(); \core\task\manager::adhoc_task_complete($task); } catch (Exception $e) { \core\task\manager::adhoc_task_failed($task); } } } /** * Helper function to call a protected/private method of an object using reflection. * * Example 1. Calling a protected object method: * $result = call_internal_method($myobject, 'method_name', [$param1, $param2], '\my\namespace\myobjectclassname'); * * Example 2. Calling a protected static method: * $result = call_internal_method(null, 'method_name', [$param1, $param2], '\my\namespace\myclassname'); * * @param object|null $object the object on which to call the method, or null if calling a static method. * @param string $methodname the name of the protected/private method. * @param array $params the array of function params to pass to the method. * @param string $classname the fully namespaced name of the class the object was created from (base in the case of mocks), * or the name of the static class when calling a static method. * @return mixed the respective return value of the method. */ public static function call_internal_method($object, $methodname, array $params, $classname) { $reflection = new \ReflectionClass($classname); $method = $reflection->getMethod($methodname); return $method->invokeArgs($object, $params); } /** * Pad the supplied string with $level levels of indentation. * * @param string $string The string to pad * @param int $level The number of levels of indentation to pad * @return string */ protected static function pad(string $string, int $level): string { return str_repeat(" ", $level * 2) . "{$string}\n"; } /** * Normalise any text to always use unix line endings (line-feeds). * * @param string $text The text to normalize * @return string */ public static function normalise_line_endings(string $text): string { return str_replace(["\r\n", "\r"], "\n", $text); } /** * Get the coverage config for the supplied includelist and excludelist configuration. * * @param string[] $includelists The list of files/folders in the includelist. * @param string[] $excludelists The list of files/folders in the excludelist. * @return string */ protected static function get_coverage_config(array $includelists, array $excludelists): string { $coverages = ''; if (!empty($includelists)) { $coverages .= self::pad("", 2); foreach ($includelists as $line) { $coverages .= self::pad($line, 3); } $coverages .= self::pad("", 2); if (!empty($excludelists)) { $coverages .= self::pad("", 2); foreach ($excludelists as $line) { $coverages .= self::pad($line, 3); } $coverages .= self::pad("", 2); } } return $coverages; } /** * Get the phpunit_coverage_info for the specified plugin or subsystem directory. * * @param string $fulldir The directory to find the coverage info file in. * @return phpunit_coverage_info */ protected static function get_coverage_info(string $fulldir): phpunit_coverage_info { $coverageconfig = "{$fulldir}/tests/coverage.php"; if (file_exists($coverageconfig)) { $coverageinfo = require($coverageconfig); if (!$coverageinfo instanceof phpunit_coverage_info) { throw new \coding_exception("{$coverageconfig} does not return a phpunit_coverage_info"); } return $coverageinfo; } return new phpunit_coverage_info();; } /** * Whether the current process is an isolated test process. * * @return bool */ public static function is_in_isolated_process(): bool { // Note: There is no function to call, or much to go by in order to tell whether we are in an isolated process // during Bootstrap, when this function is called. // We can do so by testing the existence of the wrapper function, but there is nothing set until that point. return function_exists('__phpunit_run_isolated_test'); } } classes/coverage_info.php000064400000006376152267207670011552 0ustar00. /** * Coverage information for PHPUnit. * * @package core * @category phpunit * @copyright 2018 Andrew Nicols * @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; } } classes/event_mock.php000064400000003261152267207670011064 0ustar00. /** * 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; } } classes/phpunit_dataset.php000064400000036127152267207670012135 0ustar00. /** * 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, escape: '\\') ) !== 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; } } } } } readme.md000064400000004540152267207670006344 0ustar00PHPUnit 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 bootstraplib.php000064400000006254152267207670010006 0ustar00. /** * 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); } tests/fixtures/sample_dataset_insert.xml000064400000000605152267207670014672 0ustar00 usernameemail bozka.novakova bozka@example.com pepa.novak pepa@example.com
tests/fixtures/sample_dataset_row_after_col.xml000064400000001034152267207670016210 0ustar00 tooearly idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset.txt000064400000000114152267207670013320 0ustar00username,email pepa.novak,pepa@example.com bozka.novakova,bozka@example.com tests/fixtures/sample_dataset.xml000064400000000733152267207670013310 0ustar00 idusernameemail 5 bozka.novakova bozka@example.com 7 pepa.novak pepa@example.com
tests/fixtures/sample_dataset_only_colrow.xml000064400000000737152267207670015742 0ustar00 idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset_repeated.xml000064400000001572152267207670015163 0ustar00 idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset2.xml000064400000000670152267207670013372 0ustar00 idshortnamefullname 6 101 1-0-1 8 202 2-0-2
tests/fixtures/sample_dataset_wrong_attribute.xml000064400000000735152267207670016611 0ustar00 idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset_many.xml000064400000001527152267207670014336 0ustar00 idusernameemail 5 bozka.novakova bozka@example.com 7 pepa.novak pepa@example.com
idshortnamefullname 6 101 1-0-1 8 202 2-0-2
tests/fixtures/event_fixtures.php000064400000005077152267207670013371 0ustar00. /** * 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. } } tests/fixtures/sample_dataset_wrong_table.xml000064400000000737152267207670015677 0ustar00 id username email 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com tests/fixtures/sample_dataset_wrong_dataset.xml000064400000000737152267207670016235 0ustar00 idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset_many_with_empty.xml000064400000001773152267207670016612 0ustar00 idusernameemail 5 bozka.novakova bozka@example.com 7 pepa.novak pepa@example.com
idshortnamefullname
idshortnamefullname 6 101 1-0-1 8 202 2-0-2
tests/fixtures/sample_dataset_col_before_row.xml000064400000000774152267207670016363 0ustar00 idusernameemail 5 pepa.novak pepa@example.com toolate 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset.csv000064400000000123152267207670013274 0ustar00id,username,email 5,bozka.novakova,bozka@example.com 7,pepa.novak,pepa@example.com tests/fixtures/adhoc_test_task.php000064400000002137152267207670013450 0ustar00. namespace core_phpunit; /** * Fixtures for task tests. * * @package core * @category phpunit * @copyright 2023 Andrew Lyons * @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}"); } } tests/fixtures/sample_dataset_wrong_value.xml000064400000001576152267207670015726 0ustar00 idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
idusernameemail 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/fixtures/sample_dataset_number_of_columns.xml000064400000000774152267207670017111 0ustar00 idusernameemailtoomany 5 pepa.novak pepa@example.com 7 bozka.novakova bozka@example.com
tests/advanced_test.php000064400000077127152267207670011257 0ustar00. namespace core; use core_phpunit\exception\test_exception; /** * 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 */ final class advanced_test extends \advanced_testcase { public static function setUpBeforeClass(): void { global $CFG; require_once(__DIR__ . '/fixtures/adhoc_test_task.php'); parent::setUpBeforeClass(); } 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 (test_exception $e) { $this->assertStringContainsString('unexpected database modification', $e->getMessage()); } $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 (test_exception $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 (test_exception $e) { $this->assertStringContainsString('unexpected change of $COURSE', $e->getMessage()); } $this->assertEquals(1, $SITE->id); $this->assertSame($SITE, $COURSE); $this->assertSame($SITE, $COURSE); // USER change. $this->setUser(2); try { self::resetAllData(true); } catch (test_exception $e) { $this->assertStringContainsString('unexpected change of $USER', $e->getMessage()); } $this->assertEquals(0, $USER->id); } public function test_getDataGenerator(): void { $generator = $this->getDataGenerator(); $this->assertInstanceOf('testing_data_generator', $generator); } /** * Some basic tests for the getExternalTestFileUrl() method. */ public function test_external_file_url(): void { $url = self::getExternalTestFileUrl('testfile.txt'); // There should only be a // after the protocol. $this->assertCount(2, explode('//', $url)); if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) { $this->assertStringContainsString(TEST_EXTERNAL_FILES_HTTP_URL, $url); } else { $this->assertStringContainsString('http://download.moodle.org/unittest', $url); } $url = self::getExternalTestFileUrl('testfile.txt', true); $this->assertCount(2, explode('//', $url)); if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) { $this->assertStringContainsString(TEST_EXTERNAL_FILES_HTTPS_URL, $url); } else { $this->assertStringContainsString('https://download.moodle.org/unittest', $url); } } 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 = '

message body

'; $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 = '

message body

'; $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 = '

message body

'; $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 = '

message body

'; $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()); // Catch warning for invalid 'xxx' timezone. set_error_handler(function ($errno, $errstr): void { $this->assertStringContainsString('Unknown or bad timezone', $errstr); }, E_WARNING); $this->setTimezone('xxx', 'Europe/Prague'); restore_error_handler(); $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()); // Catch warnings for other invalid cases. $invalidtimezones = ['', 'xxxx', null]; foreach ($invalidtimezones as $invalidtz) { set_error_handler(function ($errno, $errstr): void { $this->assertStringContainsString('Unknown or bad timezone', $errstr); }, E_WARNING); try { $this->setTimezone('Pacific/Auckland', $invalidtz); } catch (\Throwable $e) { $this->assertInstanceOf('PHPUnit\Framework\Error\Warning', $e); } restore_error_handler(); } } 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 (test_exception $e) { $this->assertStringContainsString('unexpected change of locale', $e->getMessage()); } 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()); $this->assertEquals('Australia/Perth', $newclock->now()->getTimezone()->getName()); // 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()); $this->assertEquals('Australia/Perth', $newclock->now()->getTimezone()->getName()); } /** * 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()); $this->assertEquals('Australia/Perth', $newclock->now()->getTimezone()->getName()); // 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()); $this->assertEquals('Australia/Perth', $newclock->now()->getTimezone()->getName()); } } tests/basic_test.php000064400000027711152267207670010565 0ustar00. namespace core; use PHPUnit\Framework\Attributes\WithoutErrorHandler; 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 */ final 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 = <<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'], "
"); $this->expectException(\PHPUnit\Framework\ExpectationFailedException::class); self::assertTag(['id' => 'testid'], "
"); } /** * 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]); $this->expectException(\core_phpunit\exception\test_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; $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; $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; $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; $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])); } /** * Test that the navigation node URL is overridden correctly. */ public function test_set_navigation_url(): void { \navigation_node::override_active_url(new \core\url('/foo/bar/baz')); $this->assertNotNull( (new \ReflectionClass(\navigation_node::class))->getStaticPropertyValue('fullmeurl', null), ); } /** * Test that the after-test teardown correctly resets the navigation node URL. * * @depends test_set_navigation_url */ public function test_navigation_url_reset(): void { $this->assertNull( (new \ReflectionClass(\navigation_node::class))->getStaticPropertyValue('fullmeurl', null), ); } } tests/phpunit_dataset_test.php000064400000107312152267207670012674 0ustar00. /** * 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 */ final 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 static function from_file_provider(): array { // 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 static function from_string_provider(): array { 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' => [ 'content' => 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' => [ 'content' => 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 static function from_array_provider(): array { 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 static function load_csv_provider(): array { 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 static function load_xml_provider(): array { 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 static function to_database_provider(): array { 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 static function get_rows_provider(): array { 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)); } } tests/util_test.php000064400000007314152267207670010456 0ustar00. namespace core; /** * Test util extra features. * * @package core * @category test * @copyright 2015 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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 static function set_table_modified_by_sql_provider(): array { 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, ), ), ); } } phpunit.xsd000064400000036404152267207670007000 0ustar00 This Schema file defines the rules by which the XML configuration file of PHPUnit 10.0 may be structured. Root Element The main type specifying the document structure