Automatic installer lang files (20100904)
[moodle.git] / lib / simpletestlib.php
blob76c89396807741c8318f792586b503abb1d5b680
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Utility functions to make unit testing easier.
21 * These functions, particularly the the database ones, are quick and
22 * dirty methods for getting things done in test cases. None of these
23 * methods should be used outside test code.
25 * Major Contirbutors
26 * - T.J.Hunt@open.ac.uk
28 * @package core
29 * @subpackage simpletestex
30 * @copyright &copy; 2006 The Open University
31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 defined('MOODLE_INTERNAL') || die();
36 /**
37 * Includes
39 require_once(dirname(__FILE__) . '/../config.php');
40 require_once($CFG->libdir . '/simpletestlib/simpletest.php');
41 require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
42 require_once($CFG->libdir . '/simpletestlib/expectation.php');
43 require_once($CFG->libdir . '/simpletestlib/reporter.php');
44 require_once($CFG->libdir . '/simpletestlib/web_tester.php');
45 require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
47 /**
48 * Recursively visit all the files in the source tree. Calls the callback
49 * function with the pathname of each file found.
51 * @param $path the folder to start searching from.
52 * @param $callback the function to call with the name of each file found.
53 * @param $fileregexp a regexp used to filter the search (optional).
54 * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
55 * only files that match the regexp will be included. (default false).
56 * @param array $ignorefolders will not go into any of these folders (optional).
58 function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
59 $files = scandir($path);
61 foreach ($files as $file) {
62 $filepath = $path .'/'. $file;
63 if (strpos($file, '.') === 0) {
64 /// Don't check hidden files.
65 continue;
66 } else if (is_dir($filepath)) {
67 if (!in_array($filepath, $ignorefolders)) {
68 recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
70 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
71 call_user_func($callback, $filepath);
76 /**
77 * An expectation for comparing strings ignoring whitespace.
79 * @package moodlecore
80 * @subpackage simpletestex
81 * @copyright &copy; 2006 The Open University
82 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
84 class IgnoreWhitespaceExpectation extends SimpleExpectation {
85 var $expect;
87 function IgnoreWhitespaceExpectation($content, $message = '%s') {
88 $this->SimpleExpectation($message);
89 $this->expect=$this->normalise($content);
92 function test($ip) {
93 return $this->normalise($ip)==$this->expect;
96 function normalise($text) {
97 return preg_replace('/\s+/m',' ',trim($text));
100 function testMessage($ip) {
101 return "Input string [$ip] doesn't match the required value.";
106 * An Expectation that two arrays contain the same list of values.
108 * @package moodlecore
109 * @subpackage simpletestex
110 * @copyright &copy; 2006 The Open University
111 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
113 class ArraysHaveSameValuesExpectation extends SimpleExpectation {
114 var $expect;
116 function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
117 $this->SimpleExpectation($message);
118 if (!is_array($expected)) {
119 trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
120 'with an expected value that is not an array.');
122 $this->expect = $this->normalise($expected);
125 function test($actual) {
126 return $this->normalise($actual) == $this->expect;
129 function normalise($array) {
130 sort($array);
131 return $array;
134 function testMessage($actual) {
135 return 'Array [' . implode(', ', $actual) .
136 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
142 * An Expectation that compares to objects, and ensures that for every field in the
143 * expected object, there is a key of the same name in the actual object, with
144 * the same value. (The actual object may have other fields to, but we ignore them.)
146 * @package moodlecore
147 * @subpackage simpletestex
148 * @copyright &copy; 2006 The Open University
149 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
151 class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
152 var $expect;
154 function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
155 $this->SimpleExpectation($message);
156 if (!is_object($expected)) {
157 trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
158 'with an expected value that is not an object.');
160 $this->expect = $expected;
163 function test($actual) {
164 foreach ($this->expect as $key => $value) {
165 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
166 // OK
167 } else if (is_null($value) && is_null($actual->$key)) {
168 // OK
169 } else {
170 return false;
173 return true;
176 function testMessage($actual) {
177 $mismatches = array();
178 foreach ($this->expect as $key => $value) {
179 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
180 // OK
181 } else if (is_null($value) && is_null($actual->$key)) {
182 // OK
183 } else {
184 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
187 return 'Actual object does not have all the same fields with the same values as the expected object (' .
188 implode(', ', $mismatches) . ').';
192 abstract class XMLStructureExpectation extends SimpleExpectation {
194 * Parse a string as XML and return a DOMDocument;
195 * @param $html
196 * @return unknown_type
198 protected function load_xml($html) {
199 $prevsetting = libxml_use_internal_errors(true);
200 $parser = new DOMDocument();
201 if (!$parser->loadXML('<html>' . $html . '</html>')) {
202 $parser = new DOMDocument();
204 libxml_clear_errors();
205 libxml_use_internal_errors($prevsetting);
206 return $parser;
210 * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
212 * @copyright 2009 Tim Hunt
213 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
215 class ContainsTagWithAttribute extends XMLStructureExpectation {
216 protected $tag;
217 protected $attribute;
218 protected $value;
220 function __construct($tag, $attribute, $value, $message = '%s') {
221 parent::__construct($message);
222 $this->tag = $tag;
223 $this->attribute = $attribute;
224 $this->value = $value;
227 function test($html) {
228 $parser = $this->load_xml($html);
229 $list = $parser->getElementsByTagName($this->tag);
231 foreach ($list as $node) {
232 if ($node->attributes->getNamedItem($this->attribute)->nodeValue == $this->value) {
233 return true;
236 return false;
239 function testMessage($html) {
240 return 'Content [' . $html . '] does not contain the tag [' .
241 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
246 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
247 * All attributes must be present and their values must match the expected values.
248 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
250 * @copyright 2009 Nicolas Connault
251 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
253 class ContainsTagWithAttributes extends XMLStructureExpectation {
255 * @var string $tag The name of the Tag to search
257 protected $tag;
259 * @var array $expectedvalues An associative array of parameters, all of which must be matched
261 protected $expectedvalues = array();
263 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
265 protected $forbiddenvalues = array();
267 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
269 protected $failurereason = 'nomatch';
271 function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
272 parent::__construct($message);
273 $this->tag = $tag;
274 $this->expectedvalues = $expectedvalues;
275 $this->forbiddenvalues = $forbiddenvalues;
278 function test($html) {
279 $parser = $this->load_xml($html);
280 $list = $parser->getElementsByTagName($this->tag);
282 $foundamatch = false;
284 // Iterating through inputs
285 foreach ($list as $node) {
286 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
287 continue;
290 // For the current expected attribute under consideration, check that values match
291 $allattributesmatch = true;
293 foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
294 if (!$node->getAttribute($expectedattribute) && $expectedvalue != '') {
295 $this->failurereason = 'nomatch';
296 continue 2; // Skip this tag, it doesn't have all the expected attributes
298 if ($node->getAttribute($expectedattribute) != $expectedvalue) {
299 $allattributesmatch = false;
300 $this->failurereason = 'nomatch';
304 if ($allattributesmatch) {
305 $foundamatch = true;
307 // Now make sure this node doesn't have any of the forbidden attributes either
308 $nodeattrlist = $node->attributes;
310 foreach ($nodeattrlist as $domattrname => $domattr) {
311 if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) == $this->forbiddenvalues[$domattrname]) {
312 $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
313 $foundamatch = false;
319 return $foundamatch;
322 function testMessage($html) {
323 $output = 'Content [' . $html . '] ';
325 if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
326 $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
327 } else if ($this->failurereason == 'nomatch') {
328 $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
329 foreach ($this->expectedvalues as $var => $val) {
330 $output .= "$var=\"$val\" ";
332 $output = rtrim($output);
333 $output .= '].';
336 return $output;
341 * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
343 * @copyright 2009 Tim Hunt
344 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
346 class ContainsTagWithContents extends XMLStructureExpectation {
347 protected $tag;
348 protected $content;
350 function __construct($tag, $content, $message = '%s') {
351 parent::__construct($message);
352 $this->tag = $tag;
353 $this->content = $content;
356 function test($html) {
357 $parser = $this->load_xml($html);
358 $list = $parser->getElementsByTagName($this->tag);
360 foreach ($list as $node) {
361 if ($node->textContent == $this->content) {
362 return true;
366 return false;
369 function testMessage($html) {
370 return 'Content [' . $html . '] does not contain the tag [' .
371 $this->tag . '] with contents [' . $this->content . '].';
376 * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
378 * @copyright 2009 Nicolas Connault
379 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
381 class ContainsEmptyTag extends XMLStructureExpectation {
382 protected $tag;
384 function __construct($tag, $message = '%s') {
385 parent::__construct($message);
386 $this->tag = $tag;
389 function test($html) {
390 $parser = $this->load_xml($html);
391 $list = $parser->getElementsByTagName($this->tag);
393 foreach ($list as $node) {
394 if (!$node->hasAttributes() && !$node->hasChildNodes()) {
395 return true;
399 return false;
402 function testMessage($html) {
403 return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
409 * This class lets you write unit tests that access a separate set of test
410 * tables with a different prefix. Only those tables you explicitly ask to
411 * be created will be.
413 * This class has failities for flipping $USER->id.
415 * The tear-down method for this class should automatically revert any changes
416 * you make during test set-up using the metods defined here. That is, it will
417 * drop tables for you automatically and revert to the real $DB and $USER->id.
419 * @package moodlecore
420 * @subpackage simpletestex
421 * @copyright &copy; 2006 The Open University
422 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
424 class UnitTestCaseUsingDatabase extends UnitTestCase {
425 private $realdb;
426 protected $testdb;
427 private $realuserid = null;
428 private $tables = array();
430 public function __construct($label = false) {
431 global $DB, $CFG;
433 // Complain if we get this far and $CFG->unittestprefix is not set.
434 if (empty($CFG->unittestprefix)) {
435 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
438 // Only do this after the above text.
439 parent::UnitTestCase($label);
441 // Create the test DB instance.
442 $this->realdb = $DB;
443 $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
444 $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
448 * Switch to using the test database for all queries until further notice.
450 protected function switch_to_test_db() {
451 global $DB;
452 if ($DB === $this->testdb) {
453 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);
455 $DB = $this->testdb;
459 * Revert to using the test database for all future queries.
461 protected function revert_to_real_db() {
462 global $DB;
463 if ($DB !== $this->testdb) {
464 debugging('revert_to_real_db called when the test DB was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
466 $DB = $this->realdb;
470 * Switch $USER->id to a test value.
472 * It might be worth making this method do more robuse $USER switching in future,
473 * however, this is sufficient for my needs at present.
475 protected function switch_global_user_id($userid) {
476 global $USER;
477 if (!is_null($this->realuserid)) {
478 debugging('switch_global_user_id called when $USER->id was already switched to a different value. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
479 } else {
480 $this->realuserid = $USER->id;
482 $USER->id = $userid;
486 * Revert $USER->id to the real value.
488 protected function revert_global_user_id() {
489 global $USER;
490 if (is_null($this->realuserid)) {
491 debugging('revert_global_user_id called without switch_global_user_id having been called first. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
492 } else {
493 $USER->id = $this->realuserid;
494 $this->realuserid = null;
499 * Check that the user has not forgotten to clean anything up, and if they
500 * have, display a rude message and clean it up for them.
502 private function automatic_clean_up() {
503 global $DB;
504 $cleanmore = false;
506 // Drop any test tables that were created.
507 foreach ($this->tables as $tablename => $notused) {
508 $this->drop_test_table($tablename);
511 // Switch back to the real DB if necessary.
512 if ($DB !== $this->realdb) {
513 $this->revert_to_real_db();
514 $cleanmore = true;
517 // revert_global_user_id if necessary.
518 if (!is_null($this->realuserid)) {
519 $this->revert_global_user_id();
520 $cleanmore = true;
523 if ($cleanmore) {
524 accesslib_clear_all_caches_for_unit_testing();
528 public function tearDown() {
529 $this->automatic_clean_up();
530 parent::tearDown();
533 public function __destruct() {
534 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
535 $this->automatic_clean_up();
539 * Create a test table just like a real one, getting getting the definition from
540 * the specified install.xml file.
541 * @param string $tablename the name of the test table.
542 * @param string $installxmlfile the install.xml file in which this table is defined.
543 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
544 * so you need only specify, for example, 'mod/quiz'.
546 protected function create_test_table($tablename, $installxmlfile) {
547 global $CFG;
548 $dbman = $this->testdb->get_manager();
549 if (isset($this->tables[$tablename])) {
550 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
551 return;
553 if ($dbman->table_exists($tablename)) {
554 debugging('This table ' . $tablename . ' already exists from a previous execution. If the error persists you will need to review your code to ensure it is being created only once.', DEBUG_DEVELOPER);
555 $dbman->drop_table(new xmldb_table($tablename));
557 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
558 $this->tables[$tablename] = 1;
562 * Convenience method for calling create_test_table repeatedly.
563 * @param array $tablenames an array of table names.
564 * @param string $installxmlfile the install.xml file in which this table is defined.
565 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
566 * so you need only specify, for example, 'mod/quiz'.
568 protected function create_test_tables($tablenames, $installxmlfile) {
569 foreach ($tablenames as $tablename) {
570 $this->create_test_table($tablename, $installxmlfile);
575 * Drop a test table.
576 * @param $tablename the name of the test table.
578 protected function drop_test_table($tablename) {
579 if (!isset($this->tables[$tablename])) {
580 debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
581 return;
583 $dbman = $this->testdb->get_manager();
584 $table = new xmldb_table($tablename);
585 $dbman->drop_table($table);
586 unset($this->tables[$tablename]);
590 * Convenience method for calling drop_test_table repeatedly.
591 * @param array $tablenames an array of table names.
593 protected function drop_test_tables($tablenames) {
594 foreach ($tablenames as $tablename) {
595 $this->drop_test_table($tablename);
600 * Load a table with some rows of data. A typical call would look like:
602 * $config = $this->load_test_data('config_plugins',
603 * array('plugin', 'name', 'value'), array(
604 * array('frog', 'numlegs', 2),
605 * array('frog', 'sound', 'croak'),
606 * array('frog', 'action', 'jump'),
607 * ));
609 * @param string $table the table name.
610 * @param array $cols the columns to fill.
611 * @param array $data the data to load.
612 * @return array $objects corresponding to $data.
614 protected function load_test_data($table, array $cols, array $data) {
615 $results = array();
616 foreach ($data as $rowid => $row) {
617 $obj = new stdClass;
618 foreach ($cols as $key => $colname) {
619 $obj->$colname = $row[$key];
621 $obj->id = $this->testdb->insert_record($table, $obj);
622 $results[$rowid] = $obj;
624 return $results;
628 * Clean up data loaded with load_test_data. The call corresponding to the
629 * example load above would be:
631 * $this->delete_test_data('config_plugins', $config);
633 * @param string $table the table name.
634 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
636 protected function delete_test_data($table, array $rows) {
637 $ids = array();
638 foreach ($rows as $row) {
639 $ids[] = $row->id;
641 $this->testdb->delete_records_list($table, 'id', $ids);
647 * @package moodlecore
648 * @subpackage simpletestex
649 * @copyright &copy; 2006 The Open University
650 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
652 class FakeDBUnitTestCase extends UnitTestCase {
653 public $tables = array();
654 public $pkfile;
655 public $cfg;
656 public $DB;
659 * In the constructor, record the max(id) of each test table into a csv file.
660 * If this file already exists, it means that a previous run of unit tests
661 * did not complete, and has left data undeleted in the DB. This data is then
662 * deleted and the file is retained. Otherwise it is created.
664 * throws moodle_exception if CSV file cannot be created
666 public function __construct($label = false) {
667 global $DB, $CFG;
669 if (empty($CFG->unittestprefix)) {
670 return;
673 parent::UnitTestCase($label);
674 // MDL-16483 Get PKs and save data to text file
676 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
677 $this->cfg = $CFG;
679 UnitTestDB::instantiate();
681 $tables = $DB->get_tables();
683 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
684 if (file_exists($this->pkfile)) {
685 $this->truncate_test_tables($this->get_table_data($this->pkfile));
687 } else { // Create the file
688 $tabledata = '';
690 foreach ($tables as $table) {
691 if ($table != 'sessions') {
692 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
693 $max_id = 0;
695 $tabledata .= "$table, $max_id\n";
698 if (!file_put_contents($this->pkfile, $tabledata)) {
699 $a = new stdClass();
700 $a->filename = $this->pkfile;
701 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
707 * Given an array of tables and their max id, truncates all test table records whose id is higher than the ones in the $tabledata array.
708 * @param array $tabledata
710 private function truncate_test_tables($tabledata) {
711 global $CFG, $DB;
713 if (empty($CFG->unittestprefix)) {
714 return;
717 $tables = $DB->get_tables();
719 foreach ($tables as $table) {
720 if ($table != 'sessions' && isset($tabledata[$table])) {
721 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
727 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
728 * 1. Table name
729 * 2. Max id
731 * throws moodle_exception if file doesn't exist
733 * @param string $filename
735 public function get_table_data($filename) {
736 global $CFG;
738 if (empty($CFG->unittestprefix)) {
739 return;
742 if (file_exists($this->pkfile)) {
743 $handle = fopen($this->pkfile, 'r');
744 $tabledata = array();
746 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
747 $tabledata[$data[0]] = $data[1];
749 return $tabledata;
750 } else {
751 $a = new stdClass();
752 $a->filename = $this->pkfile;
753 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
754 return false;
759 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
760 * Also detects if this config setting is properly set, and if the user table exists.
761 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
763 public function setUp() {
764 global $DB, $CFG;
766 if (empty($CFG->unittestprefix)) {
767 return;
770 parent::setUp();
771 $this->DB =& $DB;
772 ob_start();
776 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
778 public function tearDown() {
779 global $DB, $CFG;
781 if (empty($CFG->unittestprefix)) {
782 return;
785 if (empty($DB)) {
786 $DB = $this->DB;
788 $DB->cleanup();
789 parent::tearDown();
791 // Output buffering
792 if (ob_get_length() > 0) {
793 ob_end_flush();
798 * This will execute once all the tests have been run. It should delete the text file holding info about database contents prior to the tests
799 * It should also detect if data is missing from the original tables.
801 public function __destruct() {
802 global $CFG, $DB;
804 if (empty($CFG->unittestprefix)) {
805 return;
808 $CFG = $this->cfg;
809 $this->tearDown();
810 UnitTestDB::restore();
811 fulldelete($this->pkfile);
815 * Load a table with some rows of data. A typical call would look like:
817 * $config = $this->load_test_data('config_plugins',
818 * array('plugin', 'name', 'value'), array(
819 * array('frog', 'numlegs', 2),
820 * array('frog', 'sound', 'croak'),
821 * array('frog', 'action', 'jump'),
822 * ));
824 * @param string $table the table name.
825 * @param array $cols the columns to fill.
826 * @param array $data the data to load.
827 * @return array $objects corresponding to $data.
829 public function load_test_data($table, array $cols, array $data) {
830 global $CFG, $DB;
832 if (empty($CFG->unittestprefix)) {
833 return;
836 $results = array();
837 foreach ($data as $rowid => $row) {
838 $obj = new stdClass;
839 foreach ($cols as $key => $colname) {
840 $obj->$colname = $row[$key];
842 $obj->id = $DB->insert_record($table, $obj);
843 $results[$rowid] = $obj;
845 return $results;
849 * Clean up data loaded with load_test_data. The call corresponding to the
850 * example load above would be:
852 * $this->delete_test_data('config_plugins', $config);
854 * @param string $table the table name.
855 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
857 public function delete_test_data($table, array $rows) {
858 global $CFG, $DB;
860 if (empty($CFG->unittestprefix)) {
861 return;
864 $ids = array();
865 foreach ($rows as $row) {
866 $ids[] = $row->id;
868 $DB->delete_records_list($table, 'id', $ids);
873 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
874 * static instantiate() method, and restores the original global $DB through restore().
875 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
876 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
877 * without subclassing it.
879 * @package moodlecore
880 * @subpackage simpletestex
881 * @copyright &copy; 2006 The Open University
882 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
884 class UnitTestDB {
885 public static $DB;
886 private static $real_db;
888 public $table_data = array();
891 * Call this statically to connect to the DB using the unittest prefix, instantiate
892 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
894 public static function instantiate() {
895 global $CFG, $DB;
896 UnitTestDB::$real_db = clone($DB);
897 if (empty($CFG->unittestprefix)) {
898 print_error("prefixnotset", 'simpletest');
901 if (empty(UnitTestDB::$DB)) {
902 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
903 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
906 $manager = UnitTestDB::$DB->get_manager();
908 if (!$manager->table_exists('user')) {
909 print_error('tablesnotsetup', 'simpletest');
912 $DB = new UnitTestDB();
915 public function __call($method, $args) {
916 // Set args to null if they don't exist (up to 10 args should do)
917 if (!method_exists($this, $method)) {
918 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
919 } else {
920 call_user_func_array(array($this, $method), $args);
924 public function __get($variable) {
925 return UnitTestDB::$DB->$variable;
928 public function __set($variable, $value) {
929 UnitTestDB::$DB->$variable = $value;
932 public function __isset($variable) {
933 return isset(UnitTestDB::$DB->$variable);
936 public function __unset($variable) {
937 unset(UnitTestDB::$DB->$variable);
941 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
943 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
944 global $DB;
945 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
946 $this->table_data[$table][] = $id;
947 return $id;
951 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
952 * throw an exception and cancel update.
954 * throws moodle_exception If trying to update a record not inserted by unit tests.
956 public function update_record($table, $dataobject, $bulk=false) {
957 global $DB;
958 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
959 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
960 $a = new stdClass();
961 $a->id = $dataobject->id;
962 $a->table = $table;
963 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
964 } else {
965 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
970 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
971 * throw an exception and cancel delete.
973 * throws moodle_exception If trying to delete a record not inserted by unit tests.
975 public function delete_records($table, array $conditions=array()) {
976 global $DB;
977 $tables_to_ignore = array('context_temp');
979 $a = new stdClass();
980 $a->table = $table;
982 // Get ids matching conditions
983 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
984 return UnitTestDB::$DB->delete_records($table, $conditions);
987 $proceed_with_delete = true;
989 if (!is_array($ids_to_delete)) {
990 $ids_to_delete = array($ids_to_delete);
993 foreach ($ids_to_delete as $id) {
994 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
995 $proceed_with_delete = false;
996 $a->id = $id;
997 break;
1001 if ($proceed_with_delete) {
1002 return UnitTestDB::$DB->delete_records($table, $conditions);
1003 } else {
1004 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1009 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1010 * throw an exception and cancel delete.
1012 * throws moodle_exception If trying to delete a record not inserted by unit tests.
1014 public function delete_records_select($table, $select, array $params=null) {
1015 global $DB;
1016 $a = new stdClass();
1017 $a->table = $table;
1019 // Get ids matching conditions
1020 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1021 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1024 $proceed_with_delete = true;
1026 foreach ($ids_to_delete as $id) {
1027 if (!in_array($id, $this->table_data[$table])) {
1028 $proceed_with_delete = false;
1029 $a->id = $id;
1030 break;
1034 if ($proceed_with_delete) {
1035 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1036 } else {
1037 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1042 * Removes from the test DB all the records that were inserted during unit tests,
1044 public function cleanup() {
1045 global $DB;
1046 foreach ($this->table_data as $table => $ids) {
1047 foreach ($ids as $id) {
1048 $DB->delete_records($table, array('id' => $id));
1054 * Restores the global $DB object.
1056 public static function restore() {
1057 global $DB;
1058 $DB = UnitTestDB::$real_db;
1061 public function get_field($table, $return, array $conditions) {
1062 if (!is_array($conditions)) {
1063 throw new coding_exception('$conditions is not an array.');
1065 return UnitTestDB::$DB->get_field($table, $return, $conditions);