diff --git a/lib/phpunit/classes/advanced_testcase.php b/lib/phpunit/classes/advanced_testcase.php new file mode 100644 index 00000000000..cd0ddcd1f3e --- /dev/null +++ b/lib/phpunit/classes/advanced_testcase.php @@ -0,0 +1,320 @@ +. + +/** + * Advanced 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 + */ + + +/** + * 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 PHPUnit_Framework_TestCase { + /** @var bool automatically reset everything? null means log changes */ + private $resetAfterTest; + + /** @var moodle_transaction */ + private $testdbtransaction; + + /** + * Constructs a test case with the given name. + * + * Note: use setUp() or setUpBeforeClass() in your test cases. + * + * @param string $name + * @param array $data + * @param string $dataName + */ + final public function __construct($name = null, array $data = array(), $dataName = '') { + parent::__construct($name, $data, $dataName); + + $this->setBackupGlobals(false); + $this->setBackupStaticAttributes(false); + $this->setRunTestInSeparateProcess(false); + } + + /** + * Runs the bare test sequence. + * @return void + */ + final public function runBare() { + global $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' or $DB->get_dbfamily() === 'mssql') { + // database must allow rollback of DDL, so no mysql here + $this->testdbtransaction = $DB->start_delegated_transaction(); + } + + try { + parent::runBare(); + // set DB reference in case somebody mocked it in test + $DB = phpunit_util::get_global_backup('DB'); + } catch (Exception $e) { + // cleanup after failed expectation + phpunit_util::reset_all_data(); + throw $e; + } + + if (!$this->testdbtransaction or $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 + } + phpunit_util::reset_all_data(); + + } 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) { + phpunit_util::reset_all_data(); + throw new coding_exception('Invalid transaction state detected in test '.$this->getName()); + } + } + phpunit_util::reset_all_data(true); + } + + // make sure test did not forget to close transaction + if ($DB->is_transaction_started()) { + phpunit_util::reset_all_data(); + if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED + or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED + or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) { + throw new coding_exception('Test '.$this->getName().' did not close database transaction'); + } + } + } + + /** + * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.) + * + * @param string $xmlFile + * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet + */ + protected function createFlatXMLDataSet($xmlFile) { + return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile); + } + + /** + * Creates a new XMLDataSet with the given $xmlFile. (absolute path.) + * + * @param string $xmlFile + * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet + */ + protected function createXMLDataSet($xmlFile) { + return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile); + } + + /** + * Creates a new CsvDataSet from the given array of csv files. (absolute paths.) + * + * @param array $files array tablename=>cvsfile + * @param string $delimiter + * @param string $enclosure + * @param string $escape + * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet + */ + protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') { + $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape); + foreach($files as $table=>$file) { + $dataSet->addTable($table, $file); + } + return $dataSet; + } + + /** + * Creates new ArrayDataSet from given array + * + * @param array $data array of tables, first row in each table is columns + * @return phpunit_ArrayDataSet + */ + protected function createArrayDataSet(array $data) { + return new phpunit_ArrayDataSet($data); + } + + /** + * Load date into moodle database tables from standard PHPUnit data set. + * + * Note: it is usually better to use data generators + * + * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset + * @return void + */ + protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) { + global $DB; + + $structure = phpunit_util::get_tablestructure(); + + foreach($dataset->getTableNames() as $tablename) { + $table = $dataset->getTable($tablename); + $metadata = $dataset->getTableMetaData($tablename); + $columns = $metadata->getColumns(); + + $doimport = false; + if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) { + $doimport = in_array('id', $columns); + } + + for($r=0; $r<$table->getRowCount(); $r++) { + $record = $table->getRow($r); + if ($doimport) { + $DB->import_record($tablename, $record); + } else { + $DB->insert_record($tablename, $record); + } + } + if ($doimport) { + $DB->get_manager()->reset_sequence(new xmldb_table($tablename)); + } + } + } + + /** + * 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 and !$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; + } + + /** + * Cleanup after all tests are executed. + * + * Note: do not forget to call this if overridden... + * + * @static + * @return void + */ + public static function tearDownAfterClass() { + phpunit_util::reset_all_data(); + } + + /** + * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir. + * @static + * @return void + */ + public static function resetAllData() { + phpunit_util::reset_all_data(); + } + + /** + * Set current $USER, reset access cache. + * @static + * @param null|int|stdClass $user user record, null means non-logged-in, 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', array('id'=>$user)); + } + unset($user->description); + unset($user->access); + + session_set_user($user); + } + + /** + * Get data generator + * @static + * @return phpunit_data_generator + */ + public static function getDataGenerator() { + return phpunit_util::get_data_generator(); + } + + /** + * 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); + } + } + } +} diff --git a/lib/phpunit/classes/arraydataset.php b/lib/phpunit/classes/arraydataset.php new file mode 100644 index 00000000000..d4148f95f0c --- /dev/null +++ b/lib/phpunit/classes/arraydataset.php @@ -0,0 +1,85 @@ +. + +/** + * Array based data iterator. + * + * @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 + */ + + +/** + * Based on array iterator code from PHPUnit documentation by Sebastian Bergmann + * with new constructor parameter for different array types. + * + * @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_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet { + /** + * @var array + */ + protected $tables = array(); + + /** + * @param array $data + */ + public function __construct(array $data) { + foreach ($data AS $tableName => $rows) { + $firstrow = reset($rows); + + if (array_key_exists(0, $firstrow)) { + // columns in first row + $columnsInFirstRow = true; + $columns = $firstrow; + $key = key($rows); + unset($rows[$key]); + } else { + // column name is in each row as key + $columnsInFirstRow = false; + $columns = array_keys($firstrow); + } + + $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns); + $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData); + + foreach ($rows AS $row) { + if ($columnsInFirstRow) { + $row = array_combine($columns, $row); + } + $table->addRow($row); + } + $this->tables[$tableName] = $table; + } + } + + protected function createIterator($reverse = FALSE) { + return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse); + } + + public function getTable($tableName) { + if (!isset($this->tables[$tableName])) { + throw new InvalidArgumentException("$tableName is not a table in the current database."); + } + + return $this->tables[$tableName]; + } +} diff --git a/lib/phpunit/classes/basic_testcase.php b/lib/phpunit/classes/basic_testcase.php new file mode 100644 index 00000000000..6ca3900550a --- /dev/null +++ b/lib/phpunit/classes/basic_testcase.php @@ -0,0 +1,78 @@ +. + +/** + * Basic test case. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * The simplest PHPUnit test case customised for Moodle + * + * It is intended for isolated tests that do not modify database or any globals. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class basic_testcase extends PHPUnit_Framework_TestCase { + + /** + * Constructs a test case with the given name. + * + * Note: use setUp() or setUpBeforeClass() in your test cases. + * + * @param string $name + * @param array $data + * @param string $dataName + */ + final public function __construct($name = null, array $data = array(), $dataName = '') { + parent::__construct($name, $data, $dataName); + + $this->setBackupGlobals(false); + $this->setBackupStaticAttributes(false); + $this->setRunTestInSeparateProcess(false); + } + + /** + * Runs the bare test sequence and log any changes in global state or database. + * @return void + */ + final public function runBare() { + global $DB; + + try { + parent::runBare(); + } catch (Exception $e) { + // cleanup after failed expectation + phpunit_util::reset_all_data(); + throw $e; + } + + if ($DB->is_transaction_started()) { + phpunit_util::reset_all_data(); + throw new coding_exception('basic_testcase '.$this->getName().' is not supposed to use database transactions!'); + } + + phpunit_util::reset_all_data(true); + } +} diff --git a/lib/phpunit/classes/block_generator.php b/lib/phpunit/classes/block_generator.php new file mode 100644 index 00000000000..a1cb635180d --- /dev/null +++ b/lib/phpunit/classes/block_generator.php @@ -0,0 +1,112 @@ +. + +/** + * Block generator base 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 + */ + + +/** + * Block generator base class. + * + * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class phpunit_block_generator { + /** @var phpunit_data_generator@var */ + protected $datagenerator; + + /** @var number of created instances */ + protected $instancecount = 0; + + public function __construct(phpunit_data_generator $datagenerator) { + $this->datagenerator = $datagenerator; + } + + /** + * To be called from data reset code only, + * do not use in tests. + * @return void + */ + public function reset() { + $this->instancecount = 0; + } + + /** + * Returns block name + * @return string name of block that this class describes + * @throws coding_exception if class invalid + */ + public function get_blockname() { + $matches = null; + if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) { + throw new coding_exception('Invalid block generator class name: '.get_class($this)); + } + + if (empty($matches[1])) { + throw new coding_exception('Invalid block generator class name: '.get_class($this)); + } + return $matches[1]; + } + + /** + * Fill in record defaults + * @param stdClass $record + * @return stdClass + */ + protected function prepare_record(stdClass $record) { + $record->blockname = $this->get_blockname(); + if (!isset($record->parentcontextid)) { + $record->parentcontextid = context_system::instance()->id; + } + if (!isset($record->showinsubcontexts)) { + $record->showinsubcontexts = 1; + } + if (!isset($record->pagetypepattern)) { + $record->pagetypepattern = ''; + } + if (!isset($record->subpagepattern)) { + $record->subpagepattern = null; + } + if (!isset($record->defaultregion)) { + $record->defaultregion = ''; + } + if (!isset($record->defaultweight)) { + $record->defaultweight = ''; + } + if (!isset($record->configdata)) { + $record->configdata = null; + } + return $record; + } + + /** + * Create a test block + * @param array|stdClass $record + * @param array $options + * @return stdClass activity record + */ + abstract public function create_instance($record = null, array $options = null); +} diff --git a/lib/phpunit/classes/data_generator.php b/lib/phpunit/classes/data_generator.php new file mode 100644 index 00000000000..48a459f1d9f --- /dev/null +++ b/lib/phpunit/classes/data_generator.php @@ -0,0 +1,544 @@ +. + +/** + * Data generator. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Data generator class for unit tests and other tools + * that need to create fake test sites. + * + * @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_data_generator { + protected $usercounter = 0; + protected $categorycount = 0; + protected $coursecount = 0; + protected $scalecount = 0; + protected $groupcount = 0; + protected $groupingcount = 0; + + /** @var array list of plugin generators */ + protected $generators = array(); + + /** @var array lis of common last names */ + public $lastnames = array( + 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson', + 'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann', + 'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová', + 'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова', + '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周', + '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤', + ); + + /** @var array lis of common first names */ + public $firstnames = array( + 'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava', + 'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura', + 'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína', + 'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина', + '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏', + '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽', + ); + + public $loremipsum = <<usercounter = 0; + $this->categorycount = 0; + $this->coursecount = 0; + $this->scalecount = 0; + + foreach($this->generators as $generator) { + $generator->reset(); + } + } + + /** + * Return generator for given plugin + * @param string $component + * @return mixed plugin data generator + */ + public function get_plugin_generator($component) { + list($type, $plugin) = normalize_component($component); + + if ($type !== 'mod' and $type !== 'block') { + throw new coding_exception("Plugin type $type does not support generators yet"); + } + + $dir = get_plugin_directory($type, $plugin); + + if (!isset($this->generators[$type.'_'.$plugin])) { + $lib = "$dir/tests/generator/lib.php"; + if (!include_once($lib)) { + throw new coding_exception("Plugin $component does not support data generator, missing tests/generator/lib"); + } + $classname = $type.'_'.$plugin.'_generator'; + $this->generators[$type.'_'.$plugin] = new $classname($this); + } + + return $this->generators[$type.'_'.$plugin]; + } + + /** + * Create a test user + * @param array|stdClass $record + * @param array $options + * @return stdClass user record + */ + public function create_user($record=null, array $options=null) { + global $DB, $CFG; + + $this->usercounter++; + $i = $this->usercounter; + + $record = (array)$record; + + if (!isset($record['auth'])) { + $record['auth'] = 'manual'; + } + + if (!isset($record['firstname']) and !isset($record['lastname'])) { + $country = rand(0, 5); + $firstname = rand(0, 4); + $lastname = rand(0, 4); + $female = rand(0, 1); + $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)]; + $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)]; + + } else if (!isset($record['firstname'])) { + $record['firstname'] = 'Firstname'.$i; + + } else if (!isset($record['lastname'])) { + $record['lastname'] = 'Lastname'.$i; + } + + if (!isset($record['idnumber'])) { + $record['idnumber'] = ''; + } + + if (!isset($record['mnethostid'])) { + $record['mnethostid'] = $CFG->mnet_localhost_id; + } + + if (!isset($record['username'])) { + $record['username'] = textlib::strtolower($record['firstname']).textlib::strtolower($record['lastname']); + while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) { + $record['username'] = $record['username'].'_'.$i; + } + } + + if (!isset($record['password'])) { + $record['password'] = 'lala'; + } + + if (!isset($record['email'])) { + $record['email'] = $record['username'].'@example.com'; + } + + if (!isset($record['confirmed'])) { + $record['confirmed'] = 1; + } + + if (!isset($record['lang'])) { + $record['lang'] = 'en'; + } + + if (!isset($record['maildisplay'])) { + $record['maildisplay'] = 1; + } + + if (!isset($record['deleted'])) { + $record['deleted'] = 0; + } + + $record['timecreated'] = time(); + $record['timemodified'] = $record['timecreated']; + $record['lastip'] = '0.0.0.0'; + + $record['password'] = hash_internal_user_password($record['password']); + + if ($record['deleted']) { + $delname = $record['email'].'.'.time(); + while ($DB->record_exists('user', array('username'=>$delname))) { + $delname++; + } + $record['idnumber'] = ''; + $record['email'] = md5($record['username']); + $record['username'] = $delname; + $record['picture'] = 0; + } + + $userid = $DB->insert_record('user', $record); + + if (!$record['deleted']) { + context_user::instance($userid); + } + + return $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); + } + + /** + * Create a test course category + * @param array|stdClass $record + * @param array $options + * @return stdClass course category record + */ + function create_category($record=null, array $options=null) { + global $DB, $CFG; + require_once("$CFG->dirroot/course/lib.php"); + + $this->categorycount++; + $i = $this->categorycount; + + $record = (array)$record; + + if (!isset($record['name'])) { + $record['name'] = 'Course category '.$i; + } + + if (!isset($record['idnumber'])) { + $record['idnumber'] = ''; + } + + if (!isset($record['description'])) { + $record['description'] = "Test course category $i\n$this->loremipsum"; + } + + if (!isset($record['descriptionformat'])) { + $record['description'] = FORMAT_MOODLE; + } + + if (!isset($record['parent'])) { + $record['descriptionformat'] = 0; + } + + if (empty($record['parent'])) { + $parent = new stdClass(); + $parent->path = ''; + $parent->depth = 0; + } else { + $parent = $DB->get_record('course_categories', array('id'=>$record['parent']), '*', MUST_EXIST); + } + $record['depth'] = $parent->depth+1; + + $record['sortorder'] = 0; + $record['timemodified'] = time(); + $record['timecreated'] = $record['timemodified']; + + $catid = $DB->insert_record('course_categories', $record); + $path = $parent->path . '/' . $catid; + $DB->set_field('course_categories', 'path', $path, array('id'=>$catid)); + context_coursecat::instance($catid); + + fix_course_sortorder(); + + return $DB->get_record('course_categories', array('id'=>$catid), '*', MUST_EXIST); + } + + /** + * Create a test course + * @param array|stdClass $record + * @param array $options with keys: + * 'createsections'=>bool precreate all sections + * @return stdClass course record + */ + function create_course($record=null, array $options=null) { + global $DB, $CFG; + require_once("$CFG->dirroot/course/lib.php"); + + $this->coursecount++; + $i = $this->coursecount; + + $record = (array)$record; + + if (!isset($record['fullname'])) { + $record['fullname'] = 'Test course '.$i; + } + + if (!isset($record['shortname'])) { + $record['shortname'] = 'tc_'.$i; + } + + if (!isset($record['idnumber'])) { + $record['idnumber'] = ''; + } + + if (!isset($record['format'])) { + $record['format'] = 'topics'; + } + + if (!isset($record['newsitems'])) { + $record['newsitems'] = 0; + } + + if (!isset($record['numsections'])) { + $record['numsections'] = 5; + } + + if (!isset($record['description'])) { + $record['description'] = "Test course $i\n$this->loremipsum"; + } + + if (!isset($record['descriptionformat'])) { + $record['description'] = FORMAT_MOODLE; + } + + if (!isset($record['category'])) { + $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0"); + } + + $course = create_course((object)$record); + context_course::instance($course->id); + + if (!empty($options['createsections'])) { + for($i=1; $i<$record['numsections']; $i++) { + self::create_course_section(array('course'=>$course->id, 'section'=>$i)); + } + } + + return $course; + } + + /** + * Create course section if does not exist yet + * @param mixed $record + * @param array|null $options + * @return stdClass + * @throws coding_exception + */ + public function create_course_section($record = null, array $options = null) { + global $DB; + + $record = (array)$record; + + if (empty($record['course'])) { + throw new coding_exception('course must be present in phpunit_util::create_course_section() $record'); + } + + if (!isset($record['section'])) { + throw new coding_exception('section must be present in phpunit_util::create_course_section() $record'); + } + + if (!isset($record['name'])) { + $record['name'] = ''; + } + + if (!isset($record['summary'])) { + $record['summary'] = ''; + } + + if (!isset($record['summaryformat'])) { + $record['summaryformat'] = FORMAT_MOODLE; + } + + if ($section = $DB->get_record('course_sections', array('course'=>$record['course'], 'section'=>$record['section']))) { + return $section; + } + + $section = new stdClass(); + $section->course = $record['course']; + $section->section = $record['section']; + $section->name = $record['name']; + $section->summary = $record['summary']; + $section->summaryformat = $record['summaryformat']; + $id = $DB->insert_record('course_sections', $section); + + return $DB->get_record('course_sections', array('id'=>$id)); + } + + /** + * Create a test block + * @param string $blockname + * @param array|stdClass $record + * @param array $options + * @return stdClass block instance record + */ + public function create_block($blockname, $record=null, array $options=null) { + $generator = $this->get_plugin_generator('block_'.$blockname); + return $generator->create_instance($record, $options); + } + + /** + * Create a test module + * @param string $modulename + * @param array|stdClass $record + * @param array $options + * @return stdClass activity record + */ + public function create_module($modulename, $record=null, array $options=null) { + $generator = $this->get_plugin_generator('mod_'.$modulename); + return $generator->create_instance($record, $options); + } + + /** + * Create a test group for the specified course + * + * $record should be either an array or a stdClass containing infomation about the group to create. + * At the very least it needs to contain courseid. + * Default values are added for name, description, and descriptionformat if they are not present. + * + * This function calls {@see groups_create_group()} to create the group within the database. + * + * @param array|stdClass $record + * @return stdClass group record + */ + public function create_group($record) { + global $DB, $CFG; + + require_once($CFG->dirroot . '/group/lib.php'); + + $this->groupcount++; + $i = $this->groupcount; + + $record = (array)$record; + + if (empty($record['courseid'])) { + throw new coding_exception('courseid must be present in phpunit_util::create_group() $record'); + } + + if (!isset($record['name'])) { + $record['name'] = 'group-' . $i; + } + + if (!isset($record['description'])) { + $record['description'] = "Test Group $i\n{$this->loremipsum}"; + } + + if (!isset($record['descriptionformat'])) { + $record['descriptionformat'] = FORMAT_MOODLE; + } + + $id = groups_create_group((object)$record); + + return $DB->get_record('groups', array('id'=>$id)); + } + + /** + * Create a test grouping for the specified course + * + * $record should be either an array or a stdClass containing infomation about the grouping to create. + * At the very least it needs to contain courseid. + * Default values are added for name, description, and descriptionformat if they are not present. + * + * This function calls {@see groups_create_grouping()} to create the grouping within the database. + * + * @param array|stdClass $record + * @return stdClass grouping record + */ + public function create_grouping($record) { + global $DB, $CFG; + + require_once($CFG->dirroot . '/group/lib.php'); + + $this->groupingcount++; + $i = $this->groupingcount; + + $record = (array)$record; + + if (empty($record['courseid'])) { + throw new coding_exception('courseid must be present in phpunit_util::create_grouping() $record'); + } + + if (!isset($record['name'])) { + $record['name'] = 'grouping-' . $i; + } + + if (!isset($record['description'])) { + $record['description'] = "Test Grouping $i\n{$this->loremipsum}"; + } + + if (!isset($record['descriptionformat'])) { + $record['descriptionformat'] = FORMAT_MOODLE; + } + + $id = groups_create_grouping((object)$record); + + return $DB->get_record('groupings', array('id'=>$id)); + } + + /** + * Create a test scale + * @param array|stdClass $record + * @param array $options + * @return stdClass block instance record + */ + public function create_scale($record=null, array $options=null) { + global $DB; + + $this->scalecount++; + $i = $this->scalecount; + + $record = (array)$record; + + if (!isset($record['name'])) { + $record['name'] = 'Test scale '.$i; + } + + if (!isset($record['scale'])) { + $record['scale'] = 'A,B,C,D,F'; + } + + if (!isset($record['courseid'])) { + $record['courseid'] = 0; + } + + if (!isset($record['userid'])) { + $record['userid'] = 0; + } + + if (!isset($record['description'])) { + $record['description'] = 'Test scale description '.$i; + } + + if (!isset($record['descriptionformat'])) { + $record['descriptionformat'] = FORMAT_MOODLE; + } + + $record['timemodified'] = time(); + + if (isset($record['id'])) { + $DB->import_record('scale', $record); + $DB->get_manager()->reset_sequence('scale'); + $id = $record['id']; + } else { + $id = $DB->insert_record('scale', $record); + } + + return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST); + } +} diff --git a/lib/phpunit/classes/database_driver_testcase.php b/lib/phpunit/classes/database_driver_testcase.php new file mode 100644 index 00000000000..dc075964676 --- /dev/null +++ b/lib/phpunit/classes/database_driver_testcase.php @@ -0,0 +1,136 @@ +. + +/** + * Database driver test case. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Special test case for testing of DML drivers and DDL layer. + * + * Note: Use only 'test_table*' names when creating new tables. + * + * For DML/DDL developers: you can add following settings to config.php if you want to test different driver than the main one, + * the reason is to allow testing of incomplete drivers that do not allow full PHPUnit environment + * initialisation (the database can be empty). + * $CFG->phpunit_extra_drivers = array( + * 1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'), + * 2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'), + * 3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'), + * 4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'), + * ); + * define('PHPUNIT_TEST_DRIVER')=1; //number is index in the previous array + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class database_driver_testcase extends PHPUnit_Framework_TestCase { + /** @var moodle_database connection to extra database */ + private static $extradb = null; + + /** @var moodle_database used in these tests*/ + protected $tdb; + + /** + * Constructs a test case with the given name. + * + * @param string $name + * @param array $data + * @param string $dataName + */ + final public function __construct($name = null, array $data = array(), $dataName = '') { + parent::__construct($name, $data, $dataName); + + $this->setBackupGlobals(false); + $this->setBackupStaticAttributes(false); + $this->setRunTestInSeparateProcess(false); + } + + public static function setUpBeforeClass() { + global $CFG; + parent::setUpBeforeClass(); + + if (!defined('PHPUNIT_TEST_DRIVER')) { + // use normal $DB + return; + } + + if (!isset($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER])) { + throw new exception('Can not find driver configuration options with index: '.PHPUNIT_TEST_DRIVER); + } + + $dblibrary = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']) ? 'native' : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']; + $dbtype = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbtype']; + $dbhost = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbhost']; + $dbname = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbname']; + $dbuser = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbuser']; + $dbpass = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbpass']; + $prefix = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['prefix']; + $dboptions = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']) ? array() : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']; + + $classname = "{$dbtype}_{$dblibrary}_moodle_database"; + require_once("$CFG->libdir/dml/$classname.php"); + $d = new $classname(); + if (!$d->driver_installed()) { + throw new exception('Database driver for '.$classname.' is not installed'); + } + + $d->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); + + self::$extradb = $d; + } + + protected function setUp() { + global $DB; + parent::setUp(); + + if (self::$extradb) { + $this->tdb = self::$extradb; + } else { + $this->tdb = $DB; + } + } + + protected function tearDown() { + // delete all test tables + $dbman = $this->tdb->get_manager(); + $tables = $this->tdb->get_tables(false); + foreach($tables as $tablename) { + if (strpos($tablename, 'test_table') === 0) { + $table = new xmldb_table($tablename); + $dbman->drop_table($table); + } + } + parent::tearDown(); + } + + public static function tearDownAfterClass() { + if (self::$extradb) { + self::$extradb->dispose(); + self::$extradb = null; + } + phpunit_util::reset_all_data(); + parent::tearDownAfterClass(); + } +} diff --git a/lib/phpunit/classes/module_generator.php b/lib/phpunit/classes/module_generator.php new file mode 100644 index 00000000000..e6846575b07 --- /dev/null +++ b/lib/phpunit/classes/module_generator.php @@ -0,0 +1,141 @@ +. + +/** + * Module generator base 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 + */ + + +/** + * Module generator base class. + * + * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator. + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class phpunit_module_generator { + /** @var phpunit_data_generator@var */ + protected $datagenerator; + + /** @var number of created instances */ + protected $instancecount = 0; + + public function __construct(phpunit_data_generator $datagenerator) { + $this->datagenerator = $datagenerator; + } + + /** + * To be called from data reset code only, + * do not use in tests. + * @return void + */ + public function reset() { + $this->instancecount = 0; + } + + /** + * Returns module name + * @return string name of module that this class describes + * @throws coding_exception if class invalid + */ + public function get_modulename() { + $matches = null; + if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) { + throw new coding_exception('Invalid module generator class name: '.get_class($this)); + } + + if (empty($matches[1])) { + throw new coding_exception('Invalid module generator class name: '.get_class($this)); + } + return $matches[1]; + } + + /** + * Create course module and link it to course + * @param int $courseid + * @param array $options: section, visible + * @return int $cm instance id + */ + protected function precreate_course_module($courseid, array $options) { + global $DB, $CFG; + require_once("$CFG->dirroot/course/lib.php"); + + $modulename = $this->get_modulename(); + + $cm = new stdClass(); + $cm->course = $courseid; + $cm->module = $DB->get_field('modules', 'id', array('name'=>$modulename)); + $cm->instance = 0; + $cm->section = isset($options['section']) ? $options['section'] : 0; + $cm->idnumber = isset($options['idnumber']) ? $options['idnumber'] : 0; + $cm->added = time(); + + $columns = $DB->get_columns('course_modules'); + foreach ($options as $key=>$value) { + if ($key === 'id' or !isset($columns[$key])) { + continue; + } + if (property_exists($cm, $key)) { + continue; + } + $cm->$key = $value; + } + + $cm->id = $DB->insert_record('course_modules', $cm); + $cm->coursemodule = $cm->id; + + add_mod_to_section($cm); + + return $cm->id; + } + + /** + * Called after *_add_instance() + * @param int $id + * @param int $cmid + * @return stdClass module instance + */ + protected function post_add_instance($id, $cmid) { + global $DB; + + $DB->set_field('course_modules', 'instance', $id, array('id'=>$cmid)); + + $instance = $DB->get_record($this->get_modulename(), array('id'=>$id), '*', MUST_EXIST); + + $cm = get_coursemodule_from_id($this->get_modulename(), $cmid, $instance->course, true, MUST_EXIST); + context_module::instance($cm->id); + + $instance->cmid = $cm->id; + + return $instance; + } + + /** + * Create a test module + * @param array|stdClass $record + * @param array $options + * @return stdClass activity record + */ + abstract public function create_instance($record = null, array $options = null); +} diff --git a/lib/phpunit/classes/unittestcase.php b/lib/phpunit/classes/unittestcase.php new file mode 100644 index 00000000000..6058cc6eb93 --- /dev/null +++ b/lib/phpunit/classes/unittestcase.php @@ -0,0 +1,193 @@ +. + +/** + * Legacy SimpleTest layer. + * + * @deprecated since 2.3 + * @package core + * @category phpunit + * @author Petr Skoda + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Simplified emulation test case for legacy SimpleTest. + * + * Note: this is supposed to work for very simple tests only. + * + * @deprecated since 2.3 + * @package core + * @category phpunit + * @author Petr Skoda + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class UnitTestCase extends PHPUnit_Framework_TestCase { + + /** + * @deprecated since 2.3 + * @param bool $expected + * @param string $message + * @return void + */ + public function expectException($expected, $message = '') { + // alternatively use phpdocs: @expectedException ExceptionClassName + if (!$expected) { + return; + } + $this->setExpectedException('moodle_exception', $message); + } + + /** + * @deprecated since 2.3 + * @param bool $expected + * @param string $message + * @return void + */ + public function expectError($expected = false, $message = '') { + // alternatively use phpdocs: @expectedException PHPUnit_Framework_Error + if (!$expected) { + return; + } + $this->setExpectedException('PHPUnit_Framework_Error', $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $actual + * @param string $messages + * @return void + */ + public static function assertTrue($actual, $messages = '') { + parent::assertTrue((bool)$actual, $messages); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $actual + * @param string $messages + * @return void + */ + public static function assertFalse($actual, $messages = '') { + parent::assertFalse((bool)$actual, $messages); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertEqual($expected, $actual, $message = '') { + parent::assertEquals($expected, $actual, $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $expected + * @param mixed $actual + * @param float|int $margin + * @param string $message + * @return void + */ + public static function assertWithinMargin($expected, $actual, $margin, $message = '') { + parent::assertEquals($expected, $actual, '', $margin, $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertNotEqual($expected, $actual, $message = '') { + parent::assertNotEquals($expected, $actual, $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertIdentical($expected, $actual, $message = '') { + parent::assertSame($expected, $actual, $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertNotIdentical($expected, $actual, $message = '') { + parent::assertNotSame($expected, $actual, $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $actual + * @param mixed $expected + * @param string $message + * @return void + */ + public static function assertIsA($actual, $expected, $message = '') { + if ($expected === 'array') { + parent::assertEquals('array', gettype($actual), $message); + } else { + parent::assertInstanceOf($expected, $actual, $message); + } + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $pattern + * @param mixed $string + * @param string $message + * @return void + */ + public static function assertPattern($pattern, $string, $message = '') { + parent::assertRegExp($pattern, $string, $message); + } + + /** + * @deprecated since 2.3 + * @static + * @param mixed $pattern + * @param mixed $string + * @param string $message + * @return void + */ + public static function assertNotPattern($pattern, $string, $message = '') { + parent::assertNotRegExp($pattern, $string, $message); + } +} diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php new file mode 100644 index 00000000000..4c7ea7a85c2 --- /dev/null +++ b/lib/phpunit/classes/util.php @@ -0,0 +1,1136 @@ +. + +/** + * 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 + */ + + +/** + * 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 { + /** @var string current version hash from php files */ + protected static $versionhash = null; + + /** @var array original content of all database tables*/ + protected static $tabledata = null; + + /** @var array original structure of all database tables */ + protected static $tablestructure = null; + + /** @var array original structure of all database tables */ + protected static $sequencenames = null; + + /** @var array An array of original globals, restored after each test */ + protected static $globals = array(); + + /** @var int last value of db writes counter, used for db resetting */ + public static $lastdbwrites = null; + + /** @var phpunit_data_generator */ + protected static $generator = null; + + /** @var resource used for prevention of parallel test execution */ + protected static $lockhandle = null; + + /** + * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot. + * + * Note: do not call manually! + * + * @internal + * @static + * @return void + */ + public static function acquire_test_lock() { + global $CFG; + if (!file_exists("$CFG->phpunit_dataroot/phpunit")) { + // dataroot not initialised yet + return; + } + if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) { + file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests'); + phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock"); + } + if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) { + $wouldblock = null; + $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock); + if (!$locked) { + if ($wouldblock) { + echo "Waiting for other test execution to complete...\n"; + } + $locked = flock(self::$lockhandle, LOCK_EX); + } + if (!$locked) { + fclose(self::$lockhandle); + self::$lockhandle = null; + } + } + register_shutdown_function(array('phpunit_util', 'release_test_lock')); + } + + /** + * Note: do not call manually! + * @internal + * @static + * @return void + */ + public static function release_test_lock() { + if (self::$lockhandle) { + flock(self::$lockhandle, LOCK_UN); + fclose(self::$lockhandle); + self::$lockhandle = null; + } + } + + /** + * 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 !== phpunit_util::get_version_hash()) { + // do not set CFG - the only way forward is to drop and reinstall + return; + } + // standard CFG init + initialise_cfg(); + } + + /** + * Get data generator + * @static + * @return phpunit_data_generator + */ + public static function get_data_generator() { + if (is_null(self::$generator)) { + require_once(__DIR__.'/../generatorlib.php'); + self::$generator = new phpunit_data_generator(); + } + return self::$generator; + } + + /** + * Returns contents of all tables right after installation. + * @static + * @return array $table=>$records + */ + protected static function get_tabledata() { + global $CFG; + + if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) { + // not initialised yet + return array(); + } + + if (!isset(self::$tabledata)) { + $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser"); + self::$tabledata = unserialize($data); + } + + if (!is_array(self::$tabledata)) { + phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.'); + } + + return self::$tabledata; + } + + /** + * Returns structure of all tables right after installation. + * @static + * @return array $table=>$records + */ + public static function get_tablestructure() { + global $CFG; + + if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) { + // not initialised yet + return array(); + } + + if (!isset(self::$tablestructure)) { + $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser"); + self::$tablestructure = unserialize($data); + } + + if (!is_array(self::$tablestructure)) { + phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.'); + } + + return self::$tablestructure; + } + + /** + * Returns the names of sequences for each autoincrementing id field in all standard tables. + * @static + * @return array $table=>$sequencename + */ + public static function get_sequencenames() { + global $DB; + + if (isset(self::$sequencenames)) { + return self::$sequencenames; + } + + if (!$structure = self::get_tablestructure()) { + return array(); + } + + self::$sequencenames = array(); + foreach ($structure as $table=>$ignored) { + $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table)); + if ($name !== false) { + self::$sequencenames[$table] = $name; + } + } + + return self::$sequencenames; + } + + /** + * Returns list of tables that are unmodified and empty. + * + * @static + * @return array of table names, empty if unknown + */ + protected static function guess_unmodified_empty_tables() { + global $DB; + + $dbfamily = $DB->get_dbfamily(); + + if ($dbfamily === 'mysql') { + $empties = array(); + $prefix = $DB->get_prefix(); + $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%')); + foreach ($rs as $info) { + $table = strtolower($info->name); + if (strpos($table, $prefix) !== 0) { + // incorrect table match caused by _ + continue; + } + if (!is_null($info->auto_increment)) { + $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); + if ($info->auto_increment == 1) { + $empties[$table] = $table; + } + } + } + $rs->close(); + return $empties; + + } else if ($dbfamily === 'mssql') { + $empties = array(); + $prefix = $DB->get_prefix(); + $sql = "SELECT t.name + FROM sys.identity_columns i + JOIN sys.tables t ON t.object_id = i.object_id + WHERE t.name LIKE ? + AND i.name = 'id' + AND i.last_value IS NULL"; + $rs = $DB->get_recordset_sql($sql, array($prefix.'%')); + foreach ($rs as $info) { + $table = strtolower($info->name); + if (strpos($table, $prefix) !== 0) { + // incorrect table match caused by _ + continue; + } + $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); + $empties[$table] = $table; + } + $rs->close(); + return $empties; + + } else if ($dbfamily === 'oracle') { + $sequences = phpunit_util::get_sequencenames(); + $sequences = array_map('strtoupper', $sequences); + $lookup = array_flip($sequences); + $empties = array(); + list($seqs, $params) = $DB->get_in_or_equal($sequences); + $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $seq) { + $table = $lookup[$seq->sequence_name]; + $empties[$table] = $table; + } + $rs->close(); + return $empties; + + } else { + return array(); + } + } + + /** + * Reset all database sequences to initial values. + * + * @static + * @param array $empties tables that are known to be unmodified and empty + * @return void + */ + public static function reset_all_database_sequences(array $empties = null) { + global $DB; + + if (!$data = self::get_tabledata()) { + // not initialised yet + return; + } + if (!$structure = self::get_tablestructure()) { + // not initialised yet + return; + } + + $dbfamily = $DB->get_dbfamily(); + if ($dbfamily === 'postgres') { + $queries = array(); + $prefix = $DB->get_prefix(); + foreach ($data as $table=>$records) { + if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { + if (empty($records)) { + $nextid = 1; + } else { + $lastrecord = end($records); + $nextid = $lastrecord->id + 1; + } + $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid"; + } + } + if ($queries) { + $DB->change_database_structure(implode(';', $queries)); + } + + } else if ($dbfamily === 'mysql') { + $sequences = array(); + $prefix = $DB->get_prefix(); + $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%')); + foreach ($rs as $info) { + $table = strtolower($info->name); + if (strpos($table, $prefix) !== 0) { + // incorrect table match caused by _ + continue; + } + if (!is_null($info->auto_increment)) { + $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); + $sequences[$table] = $info->auto_increment; + } + } + $rs->close(); + $prefix = $DB->get_prefix(); + foreach ($data as $table=>$records) { + if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { + if (isset($sequences[$table])) { + if (empty($records)) { + $nextid = 1; + } else { + $lastrecord = end($records); + $nextid = $lastrecord->id + 1; + } + if ($sequences[$table] != $nextid) { + $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid"); + } + + } else { + // some problem exists, fallback to standard code + $DB->get_manager()->reset_sequence($table); + } + } + } + + } else if ($dbfamily === 'oracle') { + $sequences = phpunit_util::get_sequencenames(); + $sequences = array_map('strtoupper', $sequences); + $lookup = array_flip($sequences); + + $current = array(); + list($seqs, $params) = $DB->get_in_or_equal($sequences); + $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $seq) { + $table = $lookup[$seq->sequence_name]; + $current[$table] = $seq->last_number; + } + $rs->close(); + + foreach ($data as $table=>$records) { + if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { + $lastrecord = end($records); + if ($lastrecord) { + $nextid = $lastrecord->id + 1; + } else { + $nextid = 1; + } + if (!isset($current[$table])) { + $DB->get_manager()->reset_sequence($table); + } else if ($nextid == $current[$table]) { + continue; + } + // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle + $seqname = $sequences[$table]; + $cachesize = $DB->get_manager()->generator->sequence_cache_size; + $DB->change_database_structure("DROP SEQUENCE $seqname"); + $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize"); + } + } + + } else { + // note: does mssql support any kind of faster reset? + if (is_null($empties)) { + $empties = self::guess_unmodified_empty_tables(); + } + foreach ($data as $table=>$records) { + if (isset($empties[$table])) { + continue; + } + if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { + $DB->get_manager()->reset_sequence($table); + } + } + } + } + + /** + * 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 (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) { + return false; + } + + $tables = $DB->get_tables(false); + if (!$tables or empty($tables['config'])) { + // not installed yet + return false; + } + + if (!$data = self::get_tabledata()) { + // not initialised yet + return false; + } + if (!$structure = self::get_tablestructure()) { + // not initialised yet + return false; + } + + $empties = self::guess_unmodified_empty_tables(); + + foreach ($data as $table=>$records) { + if (empty($records)) { + if (isset($empties[$table])) { + // table was not modified and is empty + } else { + $DB->delete_records($table, array()); + } + continue; + } + + if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { + $currentrecords = $DB->get_records($table, array(), 'id ASC'); + $changed = false; + foreach ($records as $id=>$record) { + if (!isset($currentrecords[$id])) { + $changed = true; + break; + } + if ((array)$record != (array)$currentrecords[$id]) { + $changed = true; + break; + } + unset($currentrecords[$id]); + } + if (!$changed) { + if ($currentrecords) { + $lastrecord = end($records); + $DB->delete_records_select($table, "id > ?", array($lastrecord->id)); + continue; + } else { + continue; + } + } + } + + $DB->delete_records($table, array()); + foreach ($records as $record) { + $DB->import_record($table, $record, false, true); + } + } + + // reset all next record ids - aka sequences + self::reset_all_database_sequences($empties); + + // remove extra tables + foreach ($tables as $table) { + if (!isset($data[$table])) { + $DB->get_manager()->drop_table(new xmldb_table($table)); + } + } + + self::$lastdbwrites = $DB->perf_get_writes(); + + return true; + } + + /** + * Purge dataroot directory + * @static + * @return void + */ + public static function reset_dataroot() { + global $CFG; + + $handle = opendir($CFG->dataroot); + $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess'); + while (false !== ($item = readdir($handle))) { + if (in_array($item, $skip)) { + continue; + } + if (is_dir("$CFG->dataroot/$item")) { + remove_dir("$CFG->dataroot/$item", false); + } else { + unlink("$CFG->dataroot/$item"); + } + } + closedir($handle); + make_temp_directory(''); + make_cache_directory(''); + make_cache_directory('htmlpurifier'); + } + + /** + * 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 $logchanges log changes in global state and database in error log + * @return void + */ + public static function reset_all_data($logchanges = false) { + global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION; + + // 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(); + $warnings = array(); + + if ($logchanges) { + 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'; + } + } + + // restore original globals + $_SERVER = self::get_global_backup('_SERVER'); + $CFG = self::get_global_backup('CFG'); + $SITE = self::get_global_backup('SITE'); + $COURSE = $SITE; + + // reinitialise following globals + $OUTPUT = new bootstrap_renderer(); + $PAGE = new moodle_page(); + $FULLME = null; + $ME = null; + $SCRIPT = null; + $SESSION = new stdClass(); + $_SESSION['SESSION'] =& $SESSION; + + // set fresh new not-logged-in user + $user = new stdClass(); + $user->id = 0; + $user->mnethostid = $CFG->mnet_localhost_id; + session_set_user($user); + + // reset all static caches + accesslib_clear_all_caches(true); + get_string_manager()->reset_caches(); + events_get_handlers('reset'); + textlib::reset_caches(); + //TODO: add more resets here and probably refactor them to new core function + + // 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); + + // 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); + trigger_error($warnings, E_USER_WARNING); + } + } + + /** + * Called during bootstrap only! + * @internal + * @static + * @return void + */ + public static function bootstrap_init() { + global $CFG, $SITE, $DB; + + // backup the globals + self::$globals['_SERVER'] = $_SERVER; + self::$globals['CFG'] = clone($CFG); + self::$globals['SITE'] = clone($SITE); + self::$globals['DB'] = $DB; + + // refresh data in all tables, clear caches, etc. + phpunit_util::reset_all_data(); + } + + /** + * 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; + } + + /** + * Does this site (db and dataroot) appear to be used for production? + * We try very hard to prevent accidental damage done to production servers!! + * + * @static + * @return bool + */ + public static function is_test_site() { + global $DB, $CFG; + + if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) { + // this is already tested in bootstrap script, + // but anyway presence of this file means the dataroot is for testing + return false; + } + + $tables = $DB->get_tables(false); + if ($tables) { + if (!$DB->get_manager()->table_exists('config')) { + return false; + } + if (!get_config('core', 'phpunittest')) { + return false; + } + } + + return true; + } + + /** + * Is this site initialised to run unit tests? + * + * @static + * @return int array errorcode=>message, 0 means ok + */ + public static function testing_ready_problem() { + global $CFG, $DB; + + $tables = $DB->get_tables(false); + + 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'); + } + + if (empty($tables)) { + return array(PHPUNIT_EXITCODE_INSTALL, ''); + } + + if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) { + return array(PHPUNIT_EXITCODE_REINSTALL, ''); + } + + if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) { + return array(PHPUNIT_EXITCODE_REINSTALL, ''); + } + + $hash = phpunit_util::get_version_hash(); + $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt"); + + if ($hash !== $oldhash) { + return array(PHPUNIT_EXITCODE_REINSTALL, ''); + } + + $dbhash = get_config('core', 'phpunittest'); + if ($hash !== $dbhash) { + return array(PHPUNIT_EXITCODE_REINSTALL, ''); + } + + return array(0, ''); + } + + /** + * Drop all test site data. + * + * Note: To be used from CLI scripts only. + * + * @static + * @return void may terminate execution with exit code + */ + public static function drop_site() { + global $DB, $CFG; + + if (!self::is_test_site()) { + phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); + } + + // purge dataroot + self::reset_dataroot(); + phpunit_bootstrap_initdataroot($CFG->dataroot); + $keep = array('.', '..', 'lock', 'webrunner.xml'); + $files = scandir("$CFG->dataroot/phpunit"); + foreach ($files as $file) { + if (in_array($file, $keep)) { + continue; + } + $path = "$CFG->dataroot/phpunit/$file"; + if (is_dir($path)) { + remove_dir($path, false); + } else { + unlink($path); + } + } + + // drop all tables + $tables = $DB->get_tables(false); + if (isset($tables['config'])) { + // config always last to prevent problems with interrupted drops! + unset($tables['config']); + $tables['config'] = 'config'; + } + foreach ($tables as $tablename) { + $table = new xmldb_table($tablename); + $DB->get_manager()->drop_table($table); + } + } + + /** + * 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) = phpunit_util::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); + + // install timezone info + $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone'); + update_timezone_records($timezones); + + // add test db flag + $hash = phpunit_util::get_version_hash(); + set_config('phpunittest', $hash); + + // store data for all tables + $data = array(); + $structure = array(); + $tables = $DB->get_tables(); + foreach ($tables as $table) { + $columns = $DB->get_columns($table); + $structure[$table] = $columns; + if (isset($columns['id']) and $columns['id']->auto_increment) { + $data[$table] = $DB->get_records($table, array(), 'id ASC'); + } else { + // there should not be many of these + $data[$table] = $DB->get_records($table, array()); + } + } + $data = serialize($data); + file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data); + phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser"); + + $structure = serialize($structure); + file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure); + phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser"); + + // hash all plugin versions - helps with very fast detection of db structure changes + file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash); + phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash); + } + + /** + * Calculate unique version hash for all plugins and core. + * @static + * @return string sha1 hash + */ + public static function get_version_hash() { + global $CFG; + + if (self::$versionhash) { + return self::$versionhash; + } + + $versions = array(); + + // main version first + $version = null; + include($CFG->dirroot.'/version.php'); + $versions['core'] = $version; + + // modules + $mods = get_plugin_list('mod'); + ksort($mods); + foreach ($mods as $mod => $fullmod) { + $module = new stdClass(); + $module->version = null; + include($fullmod.'/version.php'); + $versions[$mod] = $module->version; + } + + // now the rest of plugins + $plugintypes = get_plugin_types(); + unset($plugintypes['mod']); + ksort($plugintypes); + foreach ($plugintypes as $type=>$unused) { + $plugs = get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug=>$fullplug) { + $plugin = new stdClass(); + $plugin->version = null; + @include($fullplug.'/version.php'); + $versions[$plug] = $plugin->version; + } + } + + self::$versionhash = sha1(serialize($versions)); + + return self::$versionhash; + } + + /** + * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files 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@ + '; + $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); + + $suites = ''; + + $plugintypes = get_plugin_types(); + ksort($plugintypes); + foreach ($plugintypes as $type=>$unused) { + $plugs = get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug=>$fullplug) { + if (!file_exists("$fullplug/tests/")) { + continue; + } + $dir = substr($fullplug, strlen($CFG->dirroot)+1); + $dir .= '/tests'; + $component = $type.'_'.$plug; + + $suite = str_replace('@component@', $component, $template); + $suite = str_replace('@dir@', $dir, $suite); + + $suites .= $suite; + } + } + + $data = preg_replace('|.*|s', $suites, $data, 1); + + $result = false; + if (is_writable($CFG->dirroot)) { + if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) { + phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml"); + } + } + + // relink - it seems that xml:base does not work in phpunit xml files, remove this nasty hack if you find a way to set xml base for relative refs + $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data); + $data = preg_replace('|([^<]+)|', + ''.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1', + $data); + file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data); + phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.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 = ' + + + . + + '; + + // Use the upstream file as source for the distributed configurations + $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); + $ftemplate = preg_replace('|', $ftemplate); + + // Get all the components + $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests(); + + // Get all the directories having tests + $directories = self::get_all_directories_with_tests(); + + // Find any directory not covered by proper components + $remaining = array_diff($directories, $components); + + // Add them to the list of components + $components += $remaining; + + // 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); + + // Apply it to the file template + $fcontents = str_replace('', $ctemplate, $ftemplate); + + // fix link to schema + $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); + $fcontents = str_replace('lib/phpunit/phpunit.xsd', str_repeat('../', $level).'lib/phpunit/phpunit.xsd', $fcontents); + $fcontents = str_replace('lib/phpunit/bootstrap.php', str_repeat('../', $level).'lib/phpunit/bootstrap.php', $fcontents); + + // Write the file + $result = false; + if (is_writable($cpath)) { + if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { + phpunit_boostrap_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"); + } + } + } + + /** + * Returns all the plugins having PHPUnit tests + * + * @return array all the plugins having PHPUnit tests + * + */ + private static function get_all_plugins_with_tests() { + $pluginswithtests = array(); + + $plugintypes = get_plugin_types(); + ksort($plugintypes); + foreach ($plugintypes as $type => $unused) { + $plugs = get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug => $fullplug) { + // Look for tests recursively + if (self::directory_has_tests($fullplug)) { + $pluginswithtests[$type . '_' . $plug] = $fullplug; + } + } + } + return $pluginswithtests; + } + + /** + * Returns all the subsystems having PHPUnit tests + * + * Note we are hacking here the list of subsystems + * to cover some well-known subsystems that are not properly + * returned by the {@link get_core_subsystems()} function. + * + * @return array all the subsystems having PHPUnit tests + */ + private static function get_all_subsystems_with_tests() { + global $CFG; + + $subsystemswithtests = array(); + + $subsystems = get_core_subsystems(); + + // Hack the list a bit to cover some well-known ones + $subsystems['backup'] = 'backup'; + $subsystems['db-dml'] = 'lib/dml'; + $subsystems['db-ddl'] = 'lib/ddl'; + + ksort($subsystems); + foreach ($subsystems as $subsys => $relsubsys) { + if ($relsubsys === null) { + continue; + } + $fullsubsys = $CFG->dirroot . '/' . $relsubsys; + if (!is_dir($fullsubsys)) { + continue; + } + // Look for tests recursively + if (self::directory_has_tests($fullsubsys)) { + $subsystemswithtests['core_' . $subsys] = $fullsubsys; + } + } + return $subsystemswithtests; + } + + /** + * Returns all the directories having tests + * + * @return array all directories having tests + */ + private static function get_all_directories_with_tests() { + global $CFG; + + $dirs = array(); + $dirite = new RecursiveDirectoryIterator($CFG->dirroot); + $iteite = new RecursiveIteratorIterator($dirite); + $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); + $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|'); + foreach ($regite as $path => $element) { + $key = dirname(dirname($path)); + $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_'); + $dirs[$key] = $value; + } + ksort($dirs); + return array_flip($dirs); + } + + /** + * Returns if a given directory has tests (recursively) + * + * @param $dir string full path to the directory to look for phpunit tests + * @return bool if a given directory has tests (true) or no (false) + */ + private static function directory_has_tests($dir) { + if (!is_dir($dir)) { + return false; + } + + $dirite = new RecursiveDirectoryIterator($dir); + $iteite = new RecursiveIteratorIterator($dirite); + $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); + $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|'); + $regite->rewind(); + if ($regite->valid()) { + return true; + } + return false; + } +} diff --git a/lib/phpunit/generatorlib.php b/lib/phpunit/generatorlib.php index 224334201d8..f6c8897fa7f 100644 --- a/lib/phpunit/generatorlib.php +++ b/lib/phpunit/generatorlib.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * PHPUnit data generator class + * PHPUnit data generator support * * @package core * @category phpunit @@ -23,728 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php! +require_once(__DIR__.'/classes/data_generator.php'); +require_once(__DIR__.'/classes/module_generator.php'); +require_once(__DIR__.'/classes/block_generator.php'); -/** - * Data generator for unit tests - * - * @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_data_generator { - protected $usercounter = 0; - protected $categorycount = 0; - protected $coursecount = 0; - protected $scalecount = 0; - protected $groupcount = 0; - protected $groupingcount = 0; - - /** @var array list of plugin generators */ - protected $generators = array(); - - /** @var array lis of common last names */ - public $lastnames = array( - 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson', - 'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann', - 'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová', - 'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова', - '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周', - '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤', - ); - - /** @var array lis of common first names */ - public $firstnames = array( - 'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava', - 'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura', - 'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína', - 'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина', - '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏', - '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽', - ); - - public $loremipsum = <<usercounter = 0; - $this->categorycount = 0; - $this->coursecount = 0; - $this->scalecount = 0; - - foreach($this->generators as $generator) { - $generator->reset(); - } - } - - /** - * Return generator for given plugin - * @param string $component - * @return mixed plugin data generator - */ - public function get_plugin_generator($component) { - list($type, $plugin) = normalize_component($component); - - if ($type !== 'mod' and $type !== 'block') { - throw new coding_exception("Plugin type $type does not support generators yet"); - } - - $dir = get_plugin_directory($type, $plugin); - - if (!isset($this->generators[$type.'_'.$plugin])) { - $lib = "$dir/tests/generator/lib.php"; - if (!include_once($lib)) { - throw new coding_exception("Plugin $component does not support data generator, missing tests/generator/lib"); - } - $classname = $type.'_'.$plugin.'_generator'; - $this->generators[$type.'_'.$plugin] = new $classname($this); - } - - return $this->generators[$type.'_'.$plugin]; - } - - /** - * Create a test user - * @param array|stdClass $record - * @param array $options - * @return stdClass user record - */ - public function create_user($record=null, array $options=null) { - global $DB, $CFG; - - $this->usercounter++; - $i = $this->usercounter; - - $record = (array)$record; - - if (!isset($record['auth'])) { - $record['auth'] = 'manual'; - } - - if (!isset($record['firstname']) and !isset($record['lastname'])) { - $country = rand(0, 5); - $firstname = rand(0, 4); - $lastname = rand(0, 4); - $female = rand(0, 1); - $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)]; - $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)]; - - } else if (!isset($record['firstname'])) { - $record['firstname'] = 'Firstname'.$i; - - } else if (!isset($record['lastname'])) { - $record['lastname'] = 'Lastname'.$i; - } - - if (!isset($record['idnumber'])) { - $record['idnumber'] = ''; - } - - if (!isset($record['mnethostid'])) { - $record['mnethostid'] = $CFG->mnet_localhost_id; - } - - if (!isset($record['username'])) { - $record['username'] = textlib::strtolower($record['firstname']).textlib::strtolower($record['lastname']); - while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) { - $record['username'] = $record['username'].'_'.$i; - } - } - - if (!isset($record['password'])) { - $record['password'] = 'lala'; - } - - if (!isset($record['email'])) { - $record['email'] = $record['username'].'@example.com'; - } - - if (!isset($record['confirmed'])) { - $record['confirmed'] = 1; - } - - if (!isset($record['lang'])) { - $record['lang'] = 'en'; - } - - if (!isset($record['maildisplay'])) { - $record['maildisplay'] = 1; - } - - if (!isset($record['deleted'])) { - $record['deleted'] = 0; - } - - $record['timecreated'] = time(); - $record['timemodified'] = $record['timecreated']; - $record['lastip'] = '0.0.0.0'; - - $record['password'] = hash_internal_user_password($record['password']); - - if ($record['deleted']) { - $delname = $record['email'].'.'.time(); - while ($DB->record_exists('user', array('username'=>$delname))) { - $delname++; - } - $record['idnumber'] = ''; - $record['email'] = md5($record['username']); - $record['username'] = $delname; - $record['picture'] = 0; - } - - $userid = $DB->insert_record('user', $record); - - if (!$record['deleted']) { - context_user::instance($userid); - } - - return $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); - } - - /** - * Create a test course category - * @param array|stdClass $record - * @param array $options - * @return stdClass course category record - */ - function create_category($record=null, array $options=null) { - global $DB, $CFG; - require_once("$CFG->dirroot/course/lib.php"); - - $this->categorycount++; - $i = $this->categorycount; - - $record = (array)$record; - - if (!isset($record['name'])) { - $record['name'] = 'Course category '.$i; - } - - if (!isset($record['idnumber'])) { - $record['idnumber'] = ''; - } - - if (!isset($record['description'])) { - $record['description'] = "Test course category $i\n$this->loremipsum"; - } - - if (!isset($record['descriptionformat'])) { - $record['description'] = FORMAT_MOODLE; - } - - if (!isset($record['parent'])) { - $record['descriptionformat'] = 0; - } - - if (empty($record['parent'])) { - $parent = new stdClass(); - $parent->path = ''; - $parent->depth = 0; - } else { - $parent = $DB->get_record('course_categories', array('id'=>$record['parent']), '*', MUST_EXIST); - } - $record['depth'] = $parent->depth+1; - - $record['sortorder'] = 0; - $record['timemodified'] = time(); - $record['timecreated'] = $record['timemodified']; - - $catid = $DB->insert_record('course_categories', $record); - $path = $parent->path . '/' . $catid; - $DB->set_field('course_categories', 'path', $path, array('id'=>$catid)); - context_coursecat::instance($catid); - - fix_course_sortorder(); - - return $DB->get_record('course_categories', array('id'=>$catid), '*', MUST_EXIST); - } - - /** - * Create a test course - * @param array|stdClass $record - * @param array $options with keys: - * 'createsections'=>bool precreate all sections - * @return stdClass course record - */ - function create_course($record=null, array $options=null) { - global $DB, $CFG; - require_once("$CFG->dirroot/course/lib.php"); - - $this->coursecount++; - $i = $this->coursecount; - - $record = (array)$record; - - if (!isset($record['fullname'])) { - $record['fullname'] = 'Test course '.$i; - } - - if (!isset($record['shortname'])) { - $record['shortname'] = 'tc_'.$i; - } - - if (!isset($record['idnumber'])) { - $record['idnumber'] = ''; - } - - if (!isset($record['format'])) { - $record['format'] = 'topics'; - } - - if (!isset($record['newsitems'])) { - $record['newsitems'] = 0; - } - - if (!isset($record['numsections'])) { - $record['numsections'] = 5; - } - - if (!isset($record['description'])) { - $record['description'] = "Test course $i\n$this->loremipsum"; - } - - if (!isset($record['descriptionformat'])) { - $record['description'] = FORMAT_MOODLE; - } - - if (!isset($record['category'])) { - $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0"); - } - - $course = create_course((object)$record); - context_course::instance($course->id); - - if (!empty($options['createsections'])) { - for($i=1; $i<$record['numsections']; $i++) { - self::create_course_section(array('course'=>$course->id, 'section'=>$i)); - } - } - - return $course; - } - - /** - * Create course section if does not exist yet - * @param mixed $record - * @param array|null $options - * @return stdClass - * @throws coding_exception - */ - public function create_course_section($record = null, array $options = null) { - global $DB; - - $record = (array)$record; - - if (empty($record['course'])) { - throw new coding_exception('course must be present in phpunit_util::create_course_section() $record'); - } - - if (!isset($record['section'])) { - throw new coding_exception('section must be present in phpunit_util::create_course_section() $record'); - } - - if (!isset($record['name'])) { - $record['name'] = ''; - } - - if (!isset($record['summary'])) { - $record['summary'] = ''; - } - - if (!isset($record['summaryformat'])) { - $record['summaryformat'] = FORMAT_MOODLE; - } - - if ($section = $DB->get_record('course_sections', array('course'=>$record['course'], 'section'=>$record['section']))) { - return $section; - } - - $section = new stdClass(); - $section->course = $record['course']; - $section->section = $record['section']; - $section->name = $record['name']; - $section->summary = $record['summary']; - $section->summaryformat = $record['summaryformat']; - $id = $DB->insert_record('course_sections', $section); - - return $DB->get_record('course_sections', array('id'=>$id)); - } - - /** - * Create a test block - * @param string $blockname - * @param array|stdClass $record - * @param array $options - * @return stdClass block instance record - */ - public function create_block($blockname, $record=null, array $options=null) { - $generator = $this->get_plugin_generator('block_'.$blockname); - return $generator->create_instance($record, $options); - } - - /** - * Create a test module - * @param string $modulename - * @param array|stdClass $record - * @param array $options - * @return stdClass activity record - */ - public function create_module($modulename, $record=null, array $options=null) { - $generator = $this->get_plugin_generator('mod_'.$modulename); - return $generator->create_instance($record, $options); - } - - /** - * Create a test group for the specified course - * - * $record should be either an array or a stdClass containing infomation about the group to create. - * At the very least it needs to contain courseid. - * Default values are added for name, description, and descriptionformat if they are not present. - * - * This function calls {@see groups_create_group()} to create the group within the database. - * - * @param array|stdClass $record - * @return stdClass group record - */ - public function create_group($record) { - global $DB, $CFG; - - require_once($CFG->dirroot . '/group/lib.php'); - - $this->groupcount++; - $i = $this->groupcount; - - $record = (array)$record; - - if (empty($record['courseid'])) { - throw new coding_exception('courseid must be present in phpunit_util::create_group() $record'); - } - - if (!isset($record['name'])) { - $record['name'] = 'group-' . $i; - } - - if (!isset($record['description'])) { - $record['description'] = "Test Group $i\n{$this->loremipsum}"; - } - - if (!isset($record['descriptionformat'])) { - $record['descriptionformat'] = FORMAT_MOODLE; - } - - $id = groups_create_group((object)$record); - - return $DB->get_record('groups', array('id'=>$id)); - } - - /** - * Create a test grouping for the specified course - * - * $record should be either an array or a stdClass containing infomation about the grouping to create. - * At the very least it needs to contain courseid. - * Default values are added for name, description, and descriptionformat if they are not present. - * - * This function calls {@see groups_create_grouping()} to create the grouping within the database. - * - * @param array|stdClass $record - * @return stdClass grouping record - */ - public function create_grouping($record) { - global $DB, $CFG; - - require_once($CFG->dirroot . '/group/lib.php'); - - $this->groupingcount++; - $i = $this->groupingcount; - - $record = (array)$record; - - if (empty($record['courseid'])) { - throw new coding_exception('courseid must be present in phpunit_util::create_grouping() $record'); - } - - if (!isset($record['name'])) { - $record['name'] = 'grouping-' . $i; - } - - if (!isset($record['description'])) { - $record['description'] = "Test Grouping $i\n{$this->loremipsum}"; - } - - if (!isset($record['descriptionformat'])) { - $record['descriptionformat'] = FORMAT_MOODLE; - } - - $id = groups_create_grouping((object)$record); - - return $DB->get_record('groupings', array('id'=>$id)); - } - - /** - * Create a test scale - * @param array|stdClass $record - * @param array $options - * @return stdClass block instance record - */ - public function create_scale($record=null, array $options=null) { - global $DB; - - $this->scalecount++; - $i = $this->scalecount; - - $record = (array)$record; - - if (!isset($record['name'])) { - $record['name'] = 'Test scale '.$i; - } - - if (!isset($record['scale'])) { - $record['scale'] = 'A,B,C,D,F'; - } - - if (!isset($record['courseid'])) { - $record['courseid'] = 0; - } - - if (!isset($record['userid'])) { - $record['userid'] = 0; - } - - if (!isset($record['description'])) { - $record['description'] = 'Test scale description '.$i; - } - - if (!isset($record['descriptionformat'])) { - $record['descriptionformat'] = FORMAT_MOODLE; - } - - $record['timemodified'] = time(); - - if (isset($record['id'])) { - $DB->import_record('scale', $record); - $DB->get_manager()->reset_sequence('scale'); - $id = $record['id']; - } else { - $id = $DB->insert_record('scale', $record); - } - - return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST); - } -} - - -/** - * Module generator base class. - * - * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator. - * - * @package core - * @category phpunit - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class phpunit_module_generator { - /** @var phpunit_data_generator@var */ - protected $datagenerator; - - /** @var number of created instances */ - protected $instancecount = 0; - - public function __construct(phpunit_data_generator $datagenerator) { - $this->datagenerator = $datagenerator; - } - - /** - * To be called from data reset code only, - * do not use in tests. - * @return void - */ - public function reset() { - $this->instancecount = 0; - } - - /** - * Returns module name - * @return string name of module that this class describes - * @throws coding_exception if class invalid - */ - public function get_modulename() { - $matches = null; - if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) { - throw new coding_exception('Invalid module generator class name: '.get_class($this)); - } - - if (empty($matches[1])) { - throw new coding_exception('Invalid module generator class name: '.get_class($this)); - } - return $matches[1]; - } - - /** - * Create course module and link it to course - * @param int $courseid - * @param array $options: section, visible - * @return int $cm instance id - */ - protected function precreate_course_module($courseid, array $options) { - global $DB, $CFG; - require_once("$CFG->dirroot/course/lib.php"); - - $modulename = $this->get_modulename(); - - $cm = new stdClass(); - $cm->course = $courseid; - $cm->module = $DB->get_field('modules', 'id', array('name'=>$modulename)); - $cm->instance = 0; - $cm->section = isset($options['section']) ? $options['section'] : 0; - $cm->idnumber = isset($options['idnumber']) ? $options['idnumber'] : 0; - $cm->added = time(); - - $columns = $DB->get_columns('course_modules'); - foreach ($options as $key=>$value) { - if ($key === 'id' or !isset($columns[$key])) { - continue; - } - if (property_exists($cm, $key)) { - continue; - } - $cm->$key = $value; - } - - $cm->id = $DB->insert_record('course_modules', $cm); - $cm->coursemodule = $cm->id; - - add_mod_to_section($cm); - - return $cm->id; - } - - /** - * Called after *_add_instance() - * @param int $id - * @param int $cmid - * @return stdClass module instance - */ - protected function post_add_instance($id, $cmid) { - global $DB; - - $DB->set_field('course_modules', 'instance', $id, array('id'=>$cmid)); - - $instance = $DB->get_record($this->get_modulename(), array('id'=>$id), '*', MUST_EXIST); - - $cm = get_coursemodule_from_id($this->get_modulename(), $cmid, $instance->course, true, MUST_EXIST); - context_module::instance($cm->id); - - $instance->cmid = $cm->id; - - return $instance; - } - - /** - * Create a test module - * @param array|stdClass $record - * @param array $options - * @return stdClass activity record - */ - abstract public function create_instance($record = null, array $options = null); -} - - -/** - * Block generator base class. - * - * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator. - * - * @package core - * @category phpunit - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class phpunit_block_generator { - /** @var phpunit_data_generator@var */ - protected $datagenerator; - - /** @var number of created instances */ - protected $instancecount = 0; - - public function __construct(phpunit_data_generator $datagenerator) { - $this->datagenerator = $datagenerator; - } - - /** - * To be called from data reset code only, - * do not use in tests. - * @return void - */ - public function reset() { - $this->instancecount = 0; - } - - /** - * Returns block name - * @return string name of block that this class describes - * @throws coding_exception if class invalid - */ - public function get_blockname() { - $matches = null; - if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) { - throw new coding_exception('Invalid block generator class name: '.get_class($this)); - } - - if (empty($matches[1])) { - throw new coding_exception('Invalid block generator class name: '.get_class($this)); - } - return $matches[1]; - } - - /** - * Fill in record defaults - * @param stdClass $record - * @return stdClass - */ - protected function prepare_record(stdClass $record) { - $record->blockname = $this->get_blockname(); - if (!isset($record->parentcontextid)) { - $record->parentcontextid = context_system::instance()->id; - } - if (!isset($record->showinsubcontexts)) { - $record->showinsubcontexts = 1; - } - if (!isset($record->pagetypepattern)) { - $record->pagetypepattern = ''; - } - if (!isset($record->subpagepattern)) { - $record->subpagepattern = null; - } - if (!isset($record->defaultregion)) { - $record->defaultregion = ''; - } - if (!isset($record->defaultweight)) { - $record->defaultweight = ''; - } - if (!isset($record->configdata)) { - $record->configdata = null; - } - return $record; - } - - /** - * Create a test block - * @param array|stdClass $record - * @param array $options - * @return stdClass activity record - */ - abstract public function create_instance($record = null, array $options = null); -} diff --git a/lib/phpunit/lib.php b/lib/phpunit/lib.php index ce05ae8a1c9..9cd8585e2c8 100644 --- a/lib/phpunit/lib.php +++ b/lib/phpunit/lib.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Various PHPUnit classes and functions + * Moodle PHPUnit integration * * @package core * @category phpunit @@ -23,1802 +23,15 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once 'PHPUnit/Autoload.php'; -require_once 'PHPUnit/Extensions/Database/Autoload.php'; +// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php! +require_once('PHPUnit/Autoload.php'); +require_once('PHPUnit/Extensions/Database/Autoload.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 { - /** @var string current version hash from php files */ - protected static $versionhash = null; +require_once(__DIR__.'/classes/util.php'); +require_once(__DIR__.'/classes/basic_testcase.php'); +require_once(__DIR__.'/classes/database_driver_testcase.php'); +require_once(__DIR__.'/classes/arraydataset.php'); +require_once(__DIR__.'/classes/advanced_testcase.php'); +require_once(__DIR__.'/classes/unittestcase.php'); - /** @var array original content of all database tables*/ - protected static $tabledata = null; - - /** @var array original structure of all database tables */ - protected static $tablestructure = null; - - /** @var array original structure of all database tables */ - protected static $sequencenames = null; - - /** @var array An array of original globals, restored after each test */ - protected static $globals = array(); - - /** @var int last value of db writes counter, used for db resetting */ - public static $lastdbwrites = null; - - /** @var phpunit_data_generator */ - protected static $generator = null; - - /** @var resource used for prevention of parallel test execution */ - protected static $lockhandle = null; - - /** - * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot. - * - * Note: do not call manually! - * - * @internal - * @static - * @return void - */ - public static function acquire_test_lock() { - global $CFG; - if (!file_exists("$CFG->phpunit_dataroot/phpunit")) { - // dataroot not initialised yet - return; - } - if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) { - file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests'); - phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock"); - } - if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) { - $wouldblock = null; - $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock); - if (!$locked) { - if ($wouldblock) { - echo "Waiting for other test execution to complete...\n"; - } - $locked = flock(self::$lockhandle, LOCK_EX); - } - if (!$locked) { - fclose(self::$lockhandle); - self::$lockhandle = null; - } - } - register_shutdown_function(array('phpunit_util', 'release_test_lock')); - } - - /** - * Note: do not call manually! - * @internal - * @static - * @return void - */ - public static function release_test_lock() { - if (self::$lockhandle) { - flock(self::$lockhandle, LOCK_UN); - fclose(self::$lockhandle); - self::$lockhandle = null; - } - } - - /** - * 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 !== phpunit_util::get_version_hash()) { - // do not set CFG - the only way forward is to drop and reinstall - return; - } - // standard CFG init - initialise_cfg(); - } - - /** - * Get data generator - * @static - * @return phpunit_data_generator - */ - public static function get_data_generator() { - if (is_null(self::$generator)) { - require_once(__DIR__.'/generatorlib.php'); - self::$generator = new phpunit_data_generator(); - } - return self::$generator; - } - - /** - * Returns contents of all tables right after installation. - * @static - * @return array $table=>$records - */ - protected static function get_tabledata() { - global $CFG; - - if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) { - // not initialised yet - return array(); - } - - if (!isset(self::$tabledata)) { - $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser"); - self::$tabledata = unserialize($data); - } - - if (!is_array(self::$tabledata)) { - phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.'); - } - - return self::$tabledata; - } - - /** - * Returns structure of all tables right after installation. - * @static - * @return array $table=>$records - */ - public static function get_tablestructure() { - global $CFG; - - if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) { - // not initialised yet - return array(); - } - - if (!isset(self::$tablestructure)) { - $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser"); - self::$tablestructure = unserialize($data); - } - - if (!is_array(self::$tablestructure)) { - phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.'); - } - - return self::$tablestructure; - } - - /** - * Returns the names of sequences for each autoincrementing id field in all standard tables. - * @static - * @return array $table=>$sequencename - */ - public static function get_sequencenames() { - global $DB; - - if (isset(self::$sequencenames)) { - return self::$sequencenames; - } - - if (!$structure = self::get_tablestructure()) { - return array(); - } - - self::$sequencenames = array(); - foreach ($structure as $table=>$ignored) { - $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table)); - if ($name !== false) { - self::$sequencenames[$table] = $name; - } - } - - return self::$sequencenames; - } - - /** - * Returns list of tables that are unmodified and empty. - * - * @static - * @return array of table names, empty if unknown - */ - protected static function guess_unmodified_empty_tables() { - global $DB; - - $dbfamily = $DB->get_dbfamily(); - - if ($dbfamily === 'mysql') { - $empties = array(); - $prefix = $DB->get_prefix(); - $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%')); - foreach ($rs as $info) { - $table = strtolower($info->name); - if (strpos($table, $prefix) !== 0) { - // incorrect table match caused by _ - continue; - } - if (!is_null($info->auto_increment)) { - $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); - if ($info->auto_increment == 1) { - $empties[$table] = $table; - } - } - } - $rs->close(); - return $empties; - - } else if ($dbfamily === 'mssql') { - $empties = array(); - $prefix = $DB->get_prefix(); - $sql = "SELECT t.name - FROM sys.identity_columns i - JOIN sys.tables t ON t.object_id = i.object_id - WHERE t.name LIKE ? - AND i.name = 'id' - AND i.last_value IS NULL"; - $rs = $DB->get_recordset_sql($sql, array($prefix.'%')); - foreach ($rs as $info) { - $table = strtolower($info->name); - if (strpos($table, $prefix) !== 0) { - // incorrect table match caused by _ - continue; - } - $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); - $empties[$table] = $table; - } - $rs->close(); - return $empties; - - } else if ($dbfamily === 'oracle') { - $sequences = phpunit_util::get_sequencenames(); - $sequences = array_map('strtoupper', $sequences); - $lookup = array_flip($sequences); - $empties = array(); - list($seqs, $params) = $DB->get_in_or_equal($sequences); - $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs"; - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $seq) { - $table = $lookup[$seq->sequence_name]; - $empties[$table] = $table; - } - $rs->close(); - return $empties; - - } else { - return array(); - } - } - - /** - * Reset all database sequences to initial values. - * - * @static - * @param array $empties tables that are known to be unmodified and empty - * @return void - */ - public static function reset_all_database_sequences(array $empties = null) { - global $DB; - - if (!$data = self::get_tabledata()) { - // not initialised yet - return; - } - if (!$structure = self::get_tablestructure()) { - // not initialised yet - return; - } - - $dbfamily = $DB->get_dbfamily(); - if ($dbfamily === 'postgres') { - $queries = array(); - $prefix = $DB->get_prefix(); - foreach ($data as $table=>$records) { - if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { - if (empty($records)) { - $nextid = 1; - } else { - $lastrecord = end($records); - $nextid = $lastrecord->id + 1; - } - $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid"; - } - } - if ($queries) { - $DB->change_database_structure(implode(';', $queries)); - } - - } else if ($dbfamily === 'mysql') { - $sequences = array(); - $prefix = $DB->get_prefix(); - $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%')); - foreach ($rs as $info) { - $table = strtolower($info->name); - if (strpos($table, $prefix) !== 0) { - // incorrect table match caused by _ - continue; - } - if (!is_null($info->auto_increment)) { - $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); - $sequences[$table] = $info->auto_increment; - } - } - $rs->close(); - $prefix = $DB->get_prefix(); - foreach ($data as $table=>$records) { - if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { - if (isset($sequences[$table])) { - if (empty($records)) { - $nextid = 1; - } else { - $lastrecord = end($records); - $nextid = $lastrecord->id + 1; - } - if ($sequences[$table] != $nextid) { - $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid"); - } - - } else { - // some problem exists, fallback to standard code - $DB->get_manager()->reset_sequence($table); - } - } - } - - } else if ($dbfamily === 'oracle') { - $sequences = phpunit_util::get_sequencenames(); - $sequences = array_map('strtoupper', $sequences); - $lookup = array_flip($sequences); - - $current = array(); - list($seqs, $params) = $DB->get_in_or_equal($sequences); - $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs"; - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $seq) { - $table = $lookup[$seq->sequence_name]; - $current[$table] = $seq->last_number; - } - $rs->close(); - - foreach ($data as $table=>$records) { - if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { - $lastrecord = end($records); - if ($lastrecord) { - $nextid = $lastrecord->id + 1; - } else { - $nextid = 1; - } - if (!isset($current[$table])) { - $DB->get_manager()->reset_sequence($table); - } else if ($nextid == $current[$table]) { - continue; - } - // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle - $seqname = $sequences[$table]; - $cachesize = $DB->get_manager()->generator->sequence_cache_size; - $DB->change_database_structure("DROP SEQUENCE $seqname"); - $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize"); - } - } - - } else { - // note: does mssql support any kind of faster reset? - if (is_null($empties)) { - $empties = self::guess_unmodified_empty_tables(); - } - foreach ($data as $table=>$records) { - if (isset($empties[$table])) { - continue; - } - if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { - $DB->get_manager()->reset_sequence($table); - } - } - } - } - - /** - * 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 (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) { - return false; - } - - $tables = $DB->get_tables(false); - if (!$tables or empty($tables['config'])) { - // not installed yet - return false; - } - - if (!$data = self::get_tabledata()) { - // not initialised yet - return false; - } - if (!$structure = self::get_tablestructure()) { - // not initialised yet - return false; - } - - $empties = self::guess_unmodified_empty_tables(); - - foreach ($data as $table=>$records) { - if (empty($records)) { - if (isset($empties[$table])) { - // table was not modified and is empty - } else { - $DB->delete_records($table, array()); - } - continue; - } - - if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { - $currentrecords = $DB->get_records($table, array(), 'id ASC'); - $changed = false; - foreach ($records as $id=>$record) { - if (!isset($currentrecords[$id])) { - $changed = true; - break; - } - if ((array)$record != (array)$currentrecords[$id]) { - $changed = true; - break; - } - unset($currentrecords[$id]); - } - if (!$changed) { - if ($currentrecords) { - $lastrecord = end($records); - $DB->delete_records_select($table, "id > ?", array($lastrecord->id)); - continue; - } else { - continue; - } - } - } - - $DB->delete_records($table, array()); - foreach ($records as $record) { - $DB->import_record($table, $record, false, true); - } - } - - // reset all next record ids - aka sequences - self::reset_all_database_sequences($empties); - - // remove extra tables - foreach ($tables as $table) { - if (!isset($data[$table])) { - $DB->get_manager()->drop_table(new xmldb_table($table)); - } - } - - self::$lastdbwrites = $DB->perf_get_writes(); - - return true; - } - - /** - * Purge dataroot directory - * @static - * @return void - */ - public static function reset_dataroot() { - global $CFG; - - $handle = opendir($CFG->dataroot); - $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess'); - while (false !== ($item = readdir($handle))) { - if (in_array($item, $skip)) { - continue; - } - if (is_dir("$CFG->dataroot/$item")) { - remove_dir("$CFG->dataroot/$item", false); - } else { - unlink("$CFG->dataroot/$item"); - } - } - closedir($handle); - make_temp_directory(''); - make_cache_directory(''); - make_cache_directory('htmlpurifier'); - } - - /** - * 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 $logchanges log changes in global state and database in error log - * @return void - */ - public static function reset_all_data($logchanges = false) { - global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION; - - // 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(); - $warnings = array(); - - if ($logchanges) { - 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'; - } - } - - // restore original globals - $_SERVER = self::get_global_backup('_SERVER'); - $CFG = self::get_global_backup('CFG'); - $SITE = self::get_global_backup('SITE'); - $COURSE = $SITE; - - // reinitialise following globals - $OUTPUT = new bootstrap_renderer(); - $PAGE = new moodle_page(); - $FULLME = null; - $ME = null; - $SCRIPT = null; - $SESSION = new stdClass(); - $_SESSION['SESSION'] =& $SESSION; - - // set fresh new not-logged-in user - $user = new stdClass(); - $user->id = 0; - $user->mnethostid = $CFG->mnet_localhost_id; - session_set_user($user); - - // reset all static caches - accesslib_clear_all_caches(true); - get_string_manager()->reset_caches(); - events_get_handlers('reset'); - textlib::reset_caches(); - //TODO: add more resets here and probably refactor them to new core function - - // 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); - - // 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); - trigger_error($warnings, E_USER_WARNING); - } - } - - /** - * Called during bootstrap only! - * @internal - * @static - * @return void - */ - public static function bootstrap_init() { - global $CFG, $SITE, $DB; - - // backup the globals - self::$globals['_SERVER'] = $_SERVER; - self::$globals['CFG'] = clone($CFG); - self::$globals['SITE'] = clone($SITE); - self::$globals['DB'] = $DB; - - // refresh data in all tables, clear caches, etc. - phpunit_util::reset_all_data(); - } - - /** - * 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; - } - - /** - * Does this site (db and dataroot) appear to be used for production? - * We try very hard to prevent accidental damage done to production servers!! - * - * @static - * @return bool - */ - public static function is_test_site() { - global $DB, $CFG; - - if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) { - // this is already tested in bootstrap script, - // but anyway presence of this file means the dataroot is for testing - return false; - } - - $tables = $DB->get_tables(false); - if ($tables) { - if (!$DB->get_manager()->table_exists('config')) { - return false; - } - if (!get_config('core', 'phpunittest')) { - return false; - } - } - - return true; - } - - /** - * Is this site initialised to run unit tests? - * - * @static - * @return int array errorcode=>message, 0 means ok - */ - public static function testing_ready_problem() { - global $CFG, $DB; - - $tables = $DB->get_tables(false); - - 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'); - } - - if (empty($tables)) { - return array(PHPUNIT_EXITCODE_INSTALL, ''); - } - - if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) { - return array(PHPUNIT_EXITCODE_REINSTALL, ''); - } - - if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) { - return array(PHPUNIT_EXITCODE_REINSTALL, ''); - } - - $hash = phpunit_util::get_version_hash(); - $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt"); - - if ($hash !== $oldhash) { - return array(PHPUNIT_EXITCODE_REINSTALL, ''); - } - - $dbhash = get_config('core', 'phpunittest'); - if ($hash !== $dbhash) { - return array(PHPUNIT_EXITCODE_REINSTALL, ''); - } - - return array(0, ''); - } - - /** - * Drop all test site data. - * - * Note: To be used from CLI scripts only. - * - * @static - * @return void may terminate execution with exit code - */ - public static function drop_site() { - global $DB, $CFG; - - if (!self::is_test_site()) { - phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!'); - } - - // purge dataroot - self::reset_dataroot(); - phpunit_bootstrap_initdataroot($CFG->dataroot); - $keep = array('.', '..', 'lock', 'webrunner.xml'); - $files = scandir("$CFG->dataroot/phpunit"); - foreach ($files as $file) { - if (in_array($file, $keep)) { - continue; - } - $path = "$CFG->dataroot/phpunit/$file"; - if (is_dir($path)) { - remove_dir($path, false); - } else { - unlink($path); - } - } - - // drop all tables - $tables = $DB->get_tables(false); - if (isset($tables['config'])) { - // config always last to prevent problems with interrupted drops! - unset($tables['config']); - $tables['config'] = 'config'; - } - foreach ($tables as $tablename) { - $table = new xmldb_table($tablename); - $DB->get_manager()->drop_table($table); - } - } - - /** - * 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) = phpunit_util::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); - - // install timezone info - $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone'); - update_timezone_records($timezones); - - // add test db flag - $hash = phpunit_util::get_version_hash(); - set_config('phpunittest', $hash); - - // store data for all tables - $data = array(); - $structure = array(); - $tables = $DB->get_tables(); - foreach ($tables as $table) { - $columns = $DB->get_columns($table); - $structure[$table] = $columns; - if (isset($columns['id']) and $columns['id']->auto_increment) { - $data[$table] = $DB->get_records($table, array(), 'id ASC'); - } else { - // there should not be many of these - $data[$table] = $DB->get_records($table, array()); - } - } - $data = serialize($data); - file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data); - phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser"); - - $structure = serialize($structure); - file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure); - phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser"); - - // hash all plugin versions - helps with very fast detection of db structure changes - file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash); - phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash); - } - - /** - * Calculate unique version hash for all plugins and core. - * @static - * @return string sha1 hash - */ - public static function get_version_hash() { - global $CFG; - - if (self::$versionhash) { - return self::$versionhash; - } - - $versions = array(); - - // main version first - $version = null; - include($CFG->dirroot.'/version.php'); - $versions['core'] = $version; - - // modules - $mods = get_plugin_list('mod'); - ksort($mods); - foreach ($mods as $mod => $fullmod) { - $module = new stdClass(); - $module->version = null; - include($fullmod.'/version.php'); - $versions[$mod] = $module->version; - } - - // now the rest of plugins - $plugintypes = get_plugin_types(); - unset($plugintypes['mod']); - ksort($plugintypes); - foreach ($plugintypes as $type=>$unused) { - $plugs = get_plugin_list($type); - ksort($plugs); - foreach ($plugs as $plug=>$fullplug) { - $plugin = new stdClass(); - $plugin->version = null; - @include($fullplug.'/version.php'); - $versions[$plug] = $plugin->version; - } - } - - self::$versionhash = sha1(serialize($versions)); - - return self::$versionhash; - } - - /** - * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files 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@ - '; - $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); - - $suites = ''; - - $plugintypes = get_plugin_types(); - ksort($plugintypes); - foreach ($plugintypes as $type=>$unused) { - $plugs = get_plugin_list($type); - ksort($plugs); - foreach ($plugs as $plug=>$fullplug) { - if (!file_exists("$fullplug/tests/")) { - continue; - } - $dir = substr($fullplug, strlen($CFG->dirroot)+1); - $dir .= '/tests'; - $component = $type.'_'.$plug; - - $suite = str_replace('@component@', $component, $template); - $suite = str_replace('@dir@', $dir, $suite); - - $suites .= $suite; - } - } - - $data = preg_replace('|.*|s', $suites, $data, 1); - - $result = false; - if (is_writable($CFG->dirroot)) { - if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) { - phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml"); - } - } - - // relink - it seems that xml:base does not work in phpunit xml files, remove this nasty hack if you find a way to set xml base for relative refs - $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data); - $data = preg_replace('|([^<]+)|', - ''.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1', - $data); - file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data); - phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.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 = ' - - - . - - '; - - // Use the upstream file as source for the distributed configurations - $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); - $ftemplate = preg_replace('|', $ftemplate); - - // Get all the components - $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests(); - - // Get all the directories having tests - $directories = self::get_all_directories_with_tests(); - - // Find any directory not covered by proper components - $remaining = array_diff($directories, $components); - - // Add them to the list of components - $components += $remaining; - - // 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); - - // Apply it to the file template - $fcontents = str_replace('', $ctemplate, $ftemplate); - - // fix link to schema - $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/'); - $fcontents = str_replace('lib/phpunit/phpunit.xsd', str_repeat('../', $level).'lib/phpunit/phpunit.xsd', $fcontents); - $fcontents = str_replace('lib/phpunit/bootstrap.php', str_repeat('../', $level).'lib/phpunit/bootstrap.php', $fcontents); - - // Write the file - $result = false; - if (is_writable($cpath)) { - if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) { - phpunit_boostrap_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"); - } - } - } - - /** - * Returns all the plugins having PHPUnit tests - * - * @return array all the plugins having PHPUnit tests - * - */ - private static function get_all_plugins_with_tests() { - $pluginswithtests = array(); - - $plugintypes = get_plugin_types(); - ksort($plugintypes); - foreach ($plugintypes as $type => $unused) { - $plugs = get_plugin_list($type); - ksort($plugs); - foreach ($plugs as $plug => $fullplug) { - // Look for tests recursively - if (self::directory_has_tests($fullplug)) { - $pluginswithtests[$type . '_' . $plug] = $fullplug; - } - } - } - return $pluginswithtests; - } - - /** - * Returns all the subsystems having PHPUnit tests - * - * Note we are hacking here the list of subsystems - * to cover some well-known subsystems that are not properly - * returned by the {@link get_core_subsystems()} function. - * - * @return array all the subsystems having PHPUnit tests - */ - private static function get_all_subsystems_with_tests() { - global $CFG; - - $subsystemswithtests = array(); - - $subsystems = get_core_subsystems(); - - // Hack the list a bit to cover some well-known ones - $subsystems['backup'] = 'backup'; - $subsystems['db-dml'] = 'lib/dml'; - $subsystems['db-ddl'] = 'lib/ddl'; - - ksort($subsystems); - foreach ($subsystems as $subsys => $relsubsys) { - if ($relsubsys === null) { - continue; - } - $fullsubsys = $CFG->dirroot . '/' . $relsubsys; - if (!is_dir($fullsubsys)) { - continue; - } - // Look for tests recursively - if (self::directory_has_tests($fullsubsys)) { - $subsystemswithtests['core_' . $subsys] = $fullsubsys; - } - } - return $subsystemswithtests; - } - - /** - * Returns all the directories having tests - * - * @return array all directories having tests - */ - private static function get_all_directories_with_tests() { - global $CFG; - - $dirs = array(); - $dirite = new RecursiveDirectoryIterator($CFG->dirroot); - $iteite = new RecursiveIteratorIterator($dirite); - $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); - $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|'); - foreach ($regite as $path => $element) { - $key = dirname(dirname($path)); - $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_'); - $dirs[$key] = $value; - } - ksort($dirs); - return array_flip($dirs); - } - - /** - * Returns if a given directory has tests (recursively) - * - * @param $dir string full path to the directory to look for phpunit tests - * @return bool if a given directory has tests (true) or no (false) - */ - private static function directory_has_tests($dir) { - if (!is_dir($dir)) { - return false; - } - - $dirite = new RecursiveDirectoryIterator($dir); - $iteite = new RecursiveIteratorIterator($dirite); - $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); - $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|'); - $regite->rewind(); - if ($regite->valid()) { - return true; - } - return false; - } -} - - -/** - * Simplified emulation test case for legacy SimpleTest. - * - * Note: this is supposed to work for very simple tests only. - * - * @deprecated since 2.3 - * @package core - * @category phpunit - * @author Petr Skoda - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class UnitTestCase extends PHPUnit_Framework_TestCase { - - /** - * @deprecated since 2.3 - * @param bool $expected - * @param string $message - * @return void - */ - public function expectException($expected, $message = '') { - // alternatively use phpdocs: @expectedException ExceptionClassName - if (!$expected) { - return; - } - $this->setExpectedException('moodle_exception', $message); - } - - /** - * @deprecated since 2.3 - * @param bool $expected - * @param string $message - * @return void - */ - public function expectError($expected = false, $message = '') { - // alternatively use phpdocs: @expectedException PHPUnit_Framework_Error - if (!$expected) { - return; - } - $this->setExpectedException('PHPUnit_Framework_Error', $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $actual - * @param string $messages - * @return void - */ - public static function assertTrue($actual, $messages = '') { - parent::assertTrue((bool)$actual, $messages); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $actual - * @param string $messages - * @return void - */ - public static function assertFalse($actual, $messages = '') { - parent::assertFalse((bool)$actual, $messages); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $expected - * @param mixed $actual - * @param string $message - * @return void - */ - public static function assertEqual($expected, $actual, $message = '') { - parent::assertEquals($expected, $actual, $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $expected - * @param mixed $actual - * @param float|int $margin - * @param string $message - * @return void - */ - public static function assertWithinMargin($expected, $actual, $margin, $message = '') { - parent::assertEquals($expected, $actual, '', $margin, $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $expected - * @param mixed $actual - * @param string $message - * @return void - */ - public static function assertNotEqual($expected, $actual, $message = '') { - parent::assertNotEquals($expected, $actual, $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $expected - * @param mixed $actual - * @param string $message - * @return void - */ - public static function assertIdentical($expected, $actual, $message = '') { - parent::assertSame($expected, $actual, $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $expected - * @param mixed $actual - * @param string $message - * @return void - */ - public static function assertNotIdentical($expected, $actual, $message = '') { - parent::assertNotSame($expected, $actual, $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $actual - * @param mixed $expected - * @param string $message - * @return void - */ - public static function assertIsA($actual, $expected, $message = '') { - if ($expected === 'array') { - parent::assertEquals('array', gettype($actual), $message); - } else { - parent::assertInstanceOf($expected, $actual, $message); - } - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $pattern - * @param mixed $string - * @param string $message - * @return void - */ - public static function assertPattern($pattern, $string, $message = '') { - parent::assertRegExp($pattern, $string, $message); - } - - /** - * @deprecated since 2.3 - * @static - * @param mixed $pattern - * @param mixed $string - * @param string $message - * @return void - */ - public static function assertNotPattern($pattern, $string, $message = '') { - parent::assertNotRegExp($pattern, $string, $message); - } -} - - -/** - * 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 PHPUnit_Framework_TestCase { - - /** - * Constructs a test case with the given name. - * - * Note: use setUp() or setUpBeforeClass() in your test cases. - * - * @param string $name - * @param array $data - * @param string $dataName - */ - final public function __construct($name = null, array $data = array(), $dataName = '') { - parent::__construct($name, $data, $dataName); - - $this->setBackupGlobals(false); - $this->setBackupStaticAttributes(false); - $this->setRunTestInSeparateProcess(false); - } - - /** - * Runs the bare test sequence and log any changes in global state or database. - * @return void - */ - final public function runBare() { - global $DB; - - try { - parent::runBare(); - } catch (Exception $e) { - // cleanup after failed expectation - phpunit_util::reset_all_data(); - throw $e; - } - - if ($DB->is_transaction_started()) { - phpunit_util::reset_all_data(); - throw new coding_exception('basic_testcase '.$this->getName().' is not supposed to use database transactions!'); - } - - phpunit_util::reset_all_data(true); - } -} - - -/** - * 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 PHPUnit_Framework_TestCase { - /** @var bool automatically reset everything? null means log changes */ - private $resetAfterTest; - - /** @var moodle_transaction */ - private $testdbtransaction; - - /** - * Constructs a test case with the given name. - * - * Note: use setUp() or setUpBeforeClass() in your test cases. - * - * @param string $name - * @param array $data - * @param string $dataName - */ - final public function __construct($name = null, array $data = array(), $dataName = '') { - parent::__construct($name, $data, $dataName); - - $this->setBackupGlobals(false); - $this->setBackupStaticAttributes(false); - $this->setRunTestInSeparateProcess(false); - } - - /** - * Runs the bare test sequence. - * @return void - */ - final public function runBare() { - global $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' or $DB->get_dbfamily() === 'mssql') { - // database must allow rollback of DDL, so no mysql here - $this->testdbtransaction = $DB->start_delegated_transaction(); - } - - try { - parent::runBare(); - // set DB reference in case somebody mocked it in test - $DB = phpunit_util::get_global_backup('DB'); - } catch (Exception $e) { - // cleanup after failed expectation - phpunit_util::reset_all_data(); - throw $e; - } - - if (!$this->testdbtransaction or $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 - } - phpunit_util::reset_all_data(); - - } 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) { - phpunit_util::reset_all_data(); - throw new coding_exception('Invalid transaction state detected in test '.$this->getName()); - } - } - phpunit_util::reset_all_data(true); - } - - // make sure test did not forget to close transaction - if ($DB->is_transaction_started()) { - phpunit_util::reset_all_data(); - if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED - or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED - or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) { - throw new coding_exception('Test '.$this->getName().' did not close database transaction'); - } - } - } - - /** - * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.) - * - * @param string $xmlFile - * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet - */ - protected function createFlatXMLDataSet($xmlFile) { - return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile); - } - - /** - * Creates a new XMLDataSet with the given $xmlFile. (absolute path.) - * - * @param string $xmlFile - * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet - */ - protected function createXMLDataSet($xmlFile) { - return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile); - } - - /** - * Creates a new CsvDataSet from the given array of csv files. (absolute paths.) - * - * @param array $files array tablename=>cvsfile - * @param string $delimiter - * @param string $enclosure - * @param string $escape - * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet - */ - protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') { - $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape); - foreach($files as $table=>$file) { - $dataSet->addTable($table, $file); - } - return $dataSet; - } - - /** - * Creates new ArrayDataSet from given array - * - * @param array $data array of tables, first row in each table is columns - * @return phpunit_ArrayDataSet - */ - protected function createArrayDataSet(array $data) { - return new phpunit_ArrayDataSet($data); - } - - /** - * Load date into moodle database tables from standard PHPUnit data set. - * - * Note: it is usually better to use data generators - * - * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset - * @return void - */ - protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) { - global $DB; - - $structure = phpunit_util::get_tablestructure(); - - foreach($dataset->getTableNames() as $tablename) { - $table = $dataset->getTable($tablename); - $metadata = $dataset->getTableMetaData($tablename); - $columns = $metadata->getColumns(); - - $doimport = false; - if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) { - $doimport = in_array('id', $columns); - } - - for($r=0; $r<$table->getRowCount(); $r++) { - $record = $table->getRow($r); - if ($doimport) { - $DB->import_record($tablename, $record); - } else { - $DB->insert_record($tablename, $record); - } - } - if ($doimport) { - $DB->get_manager()->reset_sequence(new xmldb_table($tablename)); - } - } - } - - /** - * 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 and !$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; - } - - /** - * Cleanup after all tests are executed. - * - * Note: do not forget to call this if overridden... - * - * @static - * @return void - */ - public static function tearDownAfterClass() { - phpunit_util::reset_all_data(); - } - - /** - * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir. - * @static - * @return void - */ - public static function resetAllData() { - phpunit_util::reset_all_data(); - } - - /** - * Set current $USER, reset access cache. - * @static - * @param null|int|stdClass $user user record, null means non-logged-in, 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', array('id'=>$user)); - } - unset($user->description); - unset($user->access); - - session_set_user($user); - } - - /** - * Get data generator - * @static - * @return phpunit_data_generator - */ - public static function getDataGenerator() { - return phpunit_util::get_data_generator(); - } - - /** - * 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); - } - } - } -} - - -/** - * based on array iterator code from PHPUnit documentation by Sebastian Bergmann - * and added new constructor parameter for different array types. - */ -class phpunit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet { - /** - * @var array - */ - protected $tables = array(); - - /** - * @param array $data - */ - public function __construct(array $data) { - foreach ($data AS $tableName => $rows) { - $firstrow = reset($rows); - - if (array_key_exists(0, $firstrow)) { - // columns in first row - $columnsInFirstRow = true; - $columns = $firstrow; - $key = key($rows); - unset($rows[$key]); - } else { - // column name is in each row as key - $columnsInFirstRow = false; - $columns = array_keys($firstrow); - } - - $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns); - $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData); - - foreach ($rows AS $row) { - if ($columnsInFirstRow) { - $row = array_combine($columns, $row); - } - $table->addRow($row); - } - $this->tables[$tableName] = $table; - } - } - - protected function createIterator($reverse = FALSE) { - return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse); - } - - public function getTable($tableName) { - if (!isset($this->tables[$tableName])) { - throw new InvalidArgumentException("$tableName is not a table in the current database."); - } - - return $this->tables[$tableName]; - } -} - - -/** - * Special test case for testing of DML drivers and DDL layer. - * - * Note: Use only 'test_table*' names when creating new tables. - * - * For DML/DDL developers: you can add following settings to config.php if you want to test different driver than the main one, - * the reason is to allow testing of incomplete drivers that do not allow full PHPUnit environment - * initialisation (the database can be empty). - * $CFG->phpunit_extra_drivers = array( - * 1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'), - * 2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'), - * 3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'), - * 4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'), - * ); - * define('PHPUNIT_TEST_DRIVER')=1; //number is index in the previous array - * - * @package core - * @category phpunit - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class database_driver_testcase extends PHPUnit_Framework_TestCase { - /** @var moodle_database connection to extra database */ - private static $extradb = null; - - /** @var moodle_database used in these tests*/ - protected $tdb; - - /** - * Constructs a test case with the given name. - * - * @param string $name - * @param array $data - * @param string $dataName - */ - final public function __construct($name = null, array $data = array(), $dataName = '') { - parent::__construct($name, $data, $dataName); - - $this->setBackupGlobals(false); - $this->setBackupStaticAttributes(false); - $this->setRunTestInSeparateProcess(false); - } - - public static function setUpBeforeClass() { - global $CFG; - parent::setUpBeforeClass(); - - if (!defined('PHPUNIT_TEST_DRIVER')) { - // use normal $DB - return; - } - - if (!isset($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER])) { - throw new exception('Can not find driver configuration options with index: '.PHPUNIT_TEST_DRIVER); - } - - $dblibrary = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']) ? 'native' : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']; - $dbtype = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbtype']; - $dbhost = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbhost']; - $dbname = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbname']; - $dbuser = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbuser']; - $dbpass = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbpass']; - $prefix = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['prefix']; - $dboptions = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']) ? array() : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']; - - $classname = "{$dbtype}_{$dblibrary}_moodle_database"; - require_once("$CFG->libdir/dml/$classname.php"); - $d = new $classname(); - if (!$d->driver_installed()) { - throw new exception('Database driver for '.$classname.' is not installed'); - } - - $d->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); - - self::$extradb = $d; - } - - protected function setUp() { - global $DB; - parent::setUp(); - - if (self::$extradb) { - $this->tdb = self::$extradb; - } else { - $this->tdb = $DB; - } - } - - protected function tearDown() { - // delete all test tables - $dbman = $this->tdb->get_manager(); - $tables = $this->tdb->get_tables(false); - foreach($tables as $tablename) { - if (strpos($tablename, 'test_table') === 0) { - $table = new xmldb_table($tablename); - $dbman->drop_table($table); - } - } - parent::tearDown(); - } - - public static function tearDownAfterClass() { - if (self::$extradb) { - self::$extradb->dispose(); - self::$extradb = null; - } - phpunit_util::reset_all_data(); - parent::tearDownAfterClass(); - } -} diff --git a/lib/tests/phpunit_test.php b/lib/phpunit/tests/advanced_test.php similarity index 62% rename from lib/tests/phpunit_test.php rename to lib/phpunit/tests/advanced_test.php index d24a0d1e34b..e82164ddd86 100644 --- a/lib/tests/phpunit_test.php +++ b/lib/phpunit/tests/advanced_test.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * PHPUnit integration unit tests + * PHPUnit integration tests * * @package core * @category phpunit @@ -26,135 +26,6 @@ defined('MOODLE_INTERNAL') || die(); -/** - * Test basic_testcase extra features and PHPUnit Moodle 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 - */ -class core_phpunit_basic_testcase extends basic_testcase { - - /** - * Tests that bootstrapping has occurred correctly - * @return void - */ - public function test_bootstrap() { - global $CFG; - $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() { - // 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->assertEquals($a, $b, '', 0, 10, true); - - // 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(1, '1 '); - $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()); - } - -// Uncomment following tests to see logging of unexpected changes in global state and database -/* - public function test_db_modification() { - global $DB; - $DB->set_field('user', 'confirmed', 1, array('id'=>-1)); - } - - public function test_cfg_modification() { - global $CFG; - $CFG->xx = 'yy'; - unset($CFG->admin); - $CFG->rolesactive = 0; - } - - public function test_user_modification() { - global $USER; - $USER->id = 10; - } - - public function test_course_modification() { - global $COURSE; - $COURSE->id = 10; - } - - public function test_all_modifications() { - global $DB, $CFG, $USER, $COURSE; - $DB->set_field('user', 'confirmed', 1, array('id'=>-1)); - $CFG->xx = 'yy'; - unset($CFG->admin); - $CFG->rolesactive = 0; - $USER->id = 10; - $COURSE->id = 10; - } - - public function test_transaction_problem() { - global $DB; - $DB->start_delegated_transaction(); - } -*/ -} - - /** * Test advanced_testcase extra features. * @@ -391,65 +262,3 @@ class core_phpunit_advanced_testcase extends advanced_testcase { $this->assertTrue($DB->record_exists('user', array('username'=>'onemore'))); } } - - -/** - * Test data generator - * - * @package core - * @category phpunit - * @copyright 2012 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class core_phpunit_generator_testcase extends advanced_testcase { - public function test_create() { - global $DB; - - $this->resetAfterTest(true); - $generator = $this->getDataGenerator(); - - $count = $DB->count_records('user'); - $user = $generator->create_user(); - $this->assertEquals($count+1, $DB->count_records('user')); - - $count = $DB->count_records('course_categories'); - $category = $generator->create_category(); - $this->assertEquals($count+1, $DB->count_records('course_categories')); - - $count = $DB->count_records('course'); - $course = $generator->create_course(); - $this->assertEquals($count+1, $DB->count_records('course')); - - $section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3)); - $this->assertEquals($course->id, $section->course); - - $scale = $generator->create_scale(); - $this->assertNotEmpty($scale); - } - - public function test_create_module() { - global $CFG, $SITE; - if (!file_exists("$CFG->dirroot/mod/page/")) { - $this->markTestSkipped('Can not find standard Page module'); - } - - $this->resetAfterTest(true); - $generator = $this->getDataGenerator(); - - $page = $generator->create_module('page', array('course'=>$SITE->id)); - $this->assertNotEmpty($page); - } - - public function test_create_block() { - global $CFG; - if (!file_exists("$CFG->dirroot/blocks/online_users/")) { - $this->markTestSkipped('Can not find standard Online users block'); - } - - $this->resetAfterTest(true); - $generator = $this->getDataGenerator(); - - $page = $generator->create_block('online_users'); - $this->assertNotEmpty($page); - } -} diff --git a/lib/phpunit/tests/basic_test.php b/lib/phpunit/tests/basic_test.php new file mode 100644 index 00000000000..2ad03cb0d94 --- /dev/null +++ b/lib/phpunit/tests/basic_test.php @@ -0,0 +1,155 @@ +. + +/** + * PHPUnit integration tests + * + * @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 + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Test basic_testcase extra features and PHPUnit Moodle 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 + */ +class core_phpunit_basic_testcase extends basic_testcase { + + /** + * Tests that bootstrapping has occurred correctly + * @return void + */ + public function test_bootstrap() { + global $CFG; + $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() { + // 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->assertEquals($a, $b, '', 0, 10, true); + + // 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(1, '1 '); + $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()); + } + +// Uncomment following tests to see logging of unexpected changes in global state and database + /* + public function test_db_modification() { + global $DB; + $DB->set_field('user', 'confirmed', 1, array('id'=>-1)); + } + + public function test_cfg_modification() { + global $CFG; + $CFG->xx = 'yy'; + unset($CFG->admin); + $CFG->rolesactive = 0; + } + + public function test_user_modification() { + global $USER; + $USER->id = 10; + } + + public function test_course_modification() { + global $COURSE; + $COURSE->id = 10; + } + + public function test_all_modifications() { + global $DB, $CFG, $USER, $COURSE; + $DB->set_field('user', 'confirmed', 1, array('id'=>-1)); + $CFG->xx = 'yy'; + unset($CFG->admin); + $CFG->rolesactive = 0; + $USER->id = 10; + $COURSE->id = 10; + } + + public function test_transaction_problem() { + global $DB; + $DB->start_delegated_transaction(); + } + */ +} diff --git a/lib/tests/fixtures/sample_dataset.csv b/lib/phpunit/tests/fixtures/sample_dataset.csv similarity index 100% rename from lib/tests/fixtures/sample_dataset.csv rename to lib/phpunit/tests/fixtures/sample_dataset.csv diff --git a/lib/tests/fixtures/sample_dataset.xml b/lib/phpunit/tests/fixtures/sample_dataset.xml similarity index 100% rename from lib/tests/fixtures/sample_dataset.xml rename to lib/phpunit/tests/fixtures/sample_dataset.xml diff --git a/lib/phpunit/tests/generator_test.php b/lib/phpunit/tests/generator_test.php new file mode 100644 index 00000000000..eb5df6ba0e4 --- /dev/null +++ b/lib/phpunit/tests/generator_test.php @@ -0,0 +1,88 @@ +. + +/** + * PHPUnit integration tests + * + * @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 + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Test data generator + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_phpunit_generator_testcase extends advanced_testcase { + public function test_create() { + global $DB; + + $this->resetAfterTest(true); + $generator = $this->getDataGenerator(); + + $count = $DB->count_records('user'); + $user = $generator->create_user(); + $this->assertEquals($count+1, $DB->count_records('user')); + + $count = $DB->count_records('course_categories'); + $category = $generator->create_category(); + $this->assertEquals($count+1, $DB->count_records('course_categories')); + + $count = $DB->count_records('course'); + $course = $generator->create_course(); + $this->assertEquals($count+1, $DB->count_records('course')); + + $section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3)); + $this->assertEquals($course->id, $section->course); + + $scale = $generator->create_scale(); + $this->assertNotEmpty($scale); + } + + public function test_create_module() { + global $CFG, $SITE; + if (!file_exists("$CFG->dirroot/mod/page/")) { + $this->markTestSkipped('Can not find standard Page module'); + } + + $this->resetAfterTest(true); + $generator = $this->getDataGenerator(); + + $page = $generator->create_module('page', array('course'=>$SITE->id)); + $this->assertNotEmpty($page); + } + + public function test_create_block() { + global $CFG; + if (!file_exists("$CFG->dirroot/blocks/online_users/")) { + $this->markTestSkipped('Can not find standard Online users block'); + } + + $this->resetAfterTest(true); + $generator = $this->getDataGenerator(); + + $page = $generator->create_block('online_users'); + $this->assertNotEmpty($page); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index be46f6bf505..82bf8fbaa04 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -20,6 +20,9 @@ + + lib/phpunit/tests + lib/ddl/tests lib/dml/tests