Index: lib/ddl/database_manager.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/ddl/database_manager.php,v retrieving revision 1.19 diff -u -r1.19 database_manager.php --- lib/ddl/database_manager.php 12 Jan 2009 16:52:54 -0000 1.19 +++ lib/ddl/database_manager.php 19 Mar 2009 11:47:47 -0000 @@ -381,12 +381,11 @@ } /** - * This function will load one entire XMLDB file and call install_from_xmldb_structure. - * - * @param $file full path to the XML file to be used - * @return void + * Load an install.xml file, checking that it exists, and that the structure is OK. + * @param string $file the full path to the XMLDB file. + * @return xmldb_file the loaded file. */ - public function install_from_xmldb_file($file) { + private function load_xmldb_file($file) { $xmldb_file = new xmldb_file($file); if (!$xmldb_file->fileExists()) { @@ -404,9 +403,39 @@ throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??'); } + return $xmldb_file; + } + + /** + * This function will load one entire XMLDB file and call install_from_xmldb_structure. + * + * @param $file full path to the XML file to be used + * @return void + */ + public function install_from_xmldb_file($file) { + $xmldb_file = $this->load_xmldb_file($file); + $xmldb_structure = $xmldb_file->getStructure(); + $this->install_from_xmldb_structure($xmldb_structure); + } + + /** + * This function will load one entire XMLDB file and call install_from_xmldb_structure. + * + * @param $file full path to the XML file to be used + * @param $tablename the name of the table. + */ + public function install_one_table_from_xmldb_file($file, $tablename) { + $xmldb_file = $this->load_xmldb_file($file); $xmldb_structure = $xmldb_file->getStructure(); - $this->install_from_xmldb_structure($xmldb_file->getStructure()); + $targettable = $xmldb_structure->getTable($tablename); + if (is_null($targettable)) { + throw new ddl_exception('ddlunknowntable', null, 'The table ' . $tablename . ' is not defined in file ' . $file); + } + + $tempstructure = new xmldb_structure('temp'); + $tempstructure->addTable($targettable); + $this->install_from_xmldb_structure($tempstructure); } /** Index: lib/simpletestlib/test_case.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/simpletestlib/test_case.php,v retrieving revision 1.8 diff -u -r1.8 test_case.php --- lib/simpletestlib/test_case.php 10 Jan 2009 16:06:53 -0000 1.8 +++ lib/simpletestlib/test_case.php 19 Mar 2009 11:47:47 -0000 @@ -603,6 +603,10 @@ // do not execute this test because test tables not present! unset($test); continue; + } else if ($test instanceof UnitTestCaseUsingDatabase && empty($CFG->unittestprefix)) { + // do not execute this test because test tables not present! + unset($test); + continue; } } // moodle hack end Index: admin/report/unittest/index.php =================================================================== RCS file: /cvsroot/moodle/moodle/admin/report/unittest/index.php,v retrieving revision 1.12 diff -u -r1.12 index.php --- admin/report/unittest/index.php 10 Mar 2009 07:54:15 -0000 1.12 +++ admin/report/unittest/index.php 19 Mar 2009 11:47:46 -0000 @@ -16,6 +16,10 @@ require_once('ex_simple_test.php'); require_once('ex_reporter.php'); +// Always run the unit tests in developer debug mode. +$CFG->debug = DEBUG_DEVELOPER; +error_reporting($CFG->debug); + // page parameters $path = optional_param('path', null, PARAM_PATH); $showpasses = optional_param('showpasses', false, PARAM_BOOL); @@ -32,8 +36,6 @@ // Print the header. $strtitle = get_string('unittests', $langfile); -unset($CFG->unittestprefix); // for now - until test_tables.php gets implemented - if (!is_null($path)) { // Turn off xmlstrictheaders during the unit test run. $origxmlstrictheaders = !empty($CFG->xmlstrictheaders); Index: lib/simpletestlib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/simpletestlib.php,v retrieving revision 1.26 diff -u -r1.26 simpletestlib.php --- lib/simpletestlib.php 10 Mar 2009 07:53:42 -0000 1.26 +++ lib/simpletestlib.php 19 Mar 2009 11:47:46 -0000 @@ -150,6 +150,171 @@ } } +/** + * This class lets you write unit tests that access a separate set of test + * tables with a different prefix. Only those tables you explicitly ask to + * be created will be. + */ +class UnitTestCaseUsingDatabase extends UnitTestCase { + private $realdb; + protected $testdb; + private $tables = array(); + + /** + * In the constructor, record the max(id) of each test table into a csv file. + * If this file already exists, it means that a previous run of unit tests + * did not complete, and has left data undeleted in the DB. This data is then + * deleted and the file is retained. Otherwise it is created. + * @throws moodle_exception if CSV file cannot be created + */ + public function __construct($label = false) { + global $DB, $CFG; + + if (empty($CFG->unittestprefix)) { + throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.'); + } + parent::UnitTestCase($label); + + $this->realdb = $DB; + $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary); + $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix); + } + + /** + * Switch to using the test database for all queries until further notice. + * You must remember to switch back using revert_to_real_db() before the end of the test. + */ + protected function switch_to_test_db() { + global $DB; + if ($DB === $this->testdb) { + debugging('switch_to_test_db called when the test DB was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER); + } + $DB = $this->testdb; + } + + /** + * Revert to using the test database for all future queries. + */ + protected function revert_to_real_db() { + global $DB; + if ($DB !== $this->testdb) { + debugging('revert_to_real_db called when the test DB was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER); + } + $DB = $this->realdb; + } + + /** + * Check that the user has not forgotten to clean anything up, and if they + * have, display a rude message and clean it up for them. + */ + private function emergency_clean_up() { + global $DB; + + // Check that they did not forget to drop any test tables. + if (!empty($this->tables)) { + debugging('You did not clean up all your test tables in your UnitTestCaseUsingDatabase. Tables remaining: ' . + implode(', ', array_keys($this->tables)), DEBUG_DEVELOPER); + } + foreach ($this->tables as $tablename => $notused) { + $this->drop_test_table($tablename); + } + + // Check that they did not forget to switch page to the real DB. + if ($DB !== $this->realdb) { + debugging('You did not switch back to the real database in your UnitTestCaseUsingDatabase.', DEBUG_DEVELOPER); + revert_to_real_db(); + } + } + + public function tearDown() { + $this->emergency_clean_up(); + parent::tearDown(); + } + + public function __destruct() { + $this->emergency_clean_up(); + } + + /** + * Create a test table just like a real one, getting getting the definition from + * the specified install.xml file. + * @param string $tablename the name of the test table. + * @param string $installxmlfile the install.xml file in which this table is defined. + * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended, + * so you need only specify, for example, 'mod/quiz'. + */ + protected function create_test_table($tablename, $installxmlfile) { + global $CFG; + if (isset($this->tables[$tablename])) { + debugging('You are attempting to create test table ' . $tablename . 'again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER); + return; + } + $dbman = $this->testdb->get_manager(); + $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename); + $this->tables[$tablename] = 1; + } + + /** + * Drop a test table. + * @param $tablename the name of the test table. + */ + protected function drop_test_table($tablename) { + if (!isset($this->tables[$tablename])) { + debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER); + return; + } + $dbman = $this->testdb->get_manager(); + $table = new xmldb_table($tablename); + $dbman->drop_table($table); + unset($this->tables[$tablename]); + } + + /** + * Load a table with some rows of data. A typical call would look like: + * + * $config = $this->load_test_data('config_plugins', + * array('plugin', 'name', 'value'), array( + * array('frog', 'numlegs', 2), + * array('frog', 'sound', 'croak'), + * array('frog', 'action', 'jump'), + * )); + * + * @param string $table the table name. + * @param array $cols the columns to fill. + * @param array $data the data to load. + * @return array $objects corresponding to $data. + */ + protected function load_test_data($table, array $cols, array $data) { + $results = array(); + foreach ($data as $rowid => $row) { + $obj = new stdClass; + foreach ($cols as $key => $colname) { + $obj->$colname = $row[$key]; + } + $obj->id = $this->testdb->insert_record($table, $obj); + $results[$rowid] = $obj; + } + return $results; + } + + /** + * Clean up data loaded with load_test_data. The call corresponding to the + * example load above would be: + * + * $this->delete_test_data('config_plugins', $config); + * + * @param string $table the table name. + * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used. + */ + protected function delete_test_data($table, array $rows) { + $ids = array(); + foreach ($rows as $row) { + $ids[] = $row->id; + } + $this->testdb->delete_records_list($table, 'id', $ids); + } +} + class FakeDBUnitTestCase extends UnitTestCase { public $tables = array(); public $pkfile; Index: lib/simpletest/testunittestusingdb.php =================================================================== RCS file: lib/simpletest/testunittestusingdb.php diff -N lib/simpletest/testunittestusingdb.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lib/simpletest/testunittestusingdb.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,33 @@ +testdb->get_manager(); + + $this->assertFalse($dbman->table_exists('quiz_attempts')); + $this->assertFalse($dbman->table_exists('quiz')); + $this->create_test_table('quiz_attempts', 'mod/quiz'); + $this->assertTrue($dbman->table_exists('quiz_attempts')); + $this->assertFalse($dbman->table_exists('quiz')); + + $this->load_test_data('quiz_attempts', + array('quiz', 'uniqueid', 'attempt', 'preview', 'layout'), array( + array( 1 , 1 , 1 , 0 , '1,2,3,0'), + array( 1 , 2 , 2 , 1 , '2,3,1,0'))); + + $this->switch_to_test_db(); + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + $this->assertTrue(quiz_has_attempts(1)); + $this->revert_to_real_db(); + + $this->drop_test_table('quiz_attempts'); + $this->assertFalse($dbman->table_exists('quiz_attempts')); + } + +} +?>