Merge branch 'MDL-28684b-21' of git://github.com/bostelm/moodle into MOODLE_21_STABLE
[moodle.git] / lib / simpletestlib.php
blob12b1eff2feef1319b2d16bc25c69266fba46e6aa
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 if (!isset($actual->$key)) {
184 $mismatches[] = $key . ' (expected [' . $value . '] but was missing.';
185 } else {
186 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
189 return 'Actual object does not have all the same fields with the same values as the expected object (' .
190 implode(', ', $mismatches) . ').';
194 abstract class XMLStructureExpectation extends SimpleExpectation {
196 * Parse a string as XML and return a DOMDocument;
197 * @param $html
198 * @return unknown_type
200 protected function load_xml($html) {
201 $prevsetting = libxml_use_internal_errors(true);
202 $parser = new DOMDocument();
203 if (!$parser->loadXML('<html>' . $html . '</html>')) {
204 $parser = new DOMDocument();
206 libxml_clear_errors();
207 libxml_use_internal_errors($prevsetting);
208 return $parser;
211 function testMessage($html) {
212 $parsererrors = $this->load_xml($html);
213 if (is_array($parsererrors)) {
214 foreach ($parsererrors as $key => $message) {
215 $parsererrors[$key] = $message->message;
217 return 'Could not parse XML [' . $html . '] errors were [' .
218 implode('], [', $parsererrors) . ']';
220 return $this->customMessage($html);
224 * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
226 * @copyright 2009 Tim Hunt
227 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
229 class ContainsTagWithAttribute extends XMLStructureExpectation {
230 protected $tag;
231 protected $attribute;
232 protected $value;
234 function __construct($tag, $attribute, $value, $message = '%s') {
235 parent::__construct($message);
236 $this->tag = $tag;
237 $this->attribute = $attribute;
238 $this->value = $value;
241 function test($html) {
242 $parser = $this->load_xml($html);
243 if (is_array($parser)) {
244 return false;
246 $list = $parser->getElementsByTagName($this->tag);
248 foreach ($list as $node) {
249 if ($node->attributes->getNamedItem($this->attribute)->nodeValue === (string) $this->value) {
250 return true;
253 return false;
256 function customMessage($html) {
257 return 'Content [' . $html . '] does not contain the tag [' .
258 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
263 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
264 * All attributes must be present and their values must match the expected values.
265 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
267 * @copyright 2009 Nicolas Connault
268 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
270 class ContainsTagWithAttributes extends XMLStructureExpectation {
272 * @var string $tag The name of the Tag to search
274 protected $tag;
276 * @var array $expectedvalues An associative array of parameters, all of which must be matched
278 protected $expectedvalues = array();
280 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
282 protected $forbiddenvalues = array();
284 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
286 protected $failurereason = 'nomatch';
288 function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
289 parent::__construct($message);
290 $this->tag = $tag;
291 $this->expectedvalues = $expectedvalues;
292 $this->forbiddenvalues = $forbiddenvalues;
295 function test($html) {
296 $parser = $this->load_xml($html);
297 if (is_array($parser)) {
298 return false;
301 $list = $parser->getElementsByTagName($this->tag);
302 $foundamatch = false;
304 // Iterating through inputs
305 foreach ($list as $node) {
306 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
307 continue;
310 // For the current expected attribute under consideration, check that values match
311 $allattributesmatch = true;
313 foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
314 if ($node->getAttribute($expectedattribute) === '' && $expectedvalue !== '') {
315 $this->failurereason = 'nomatch';
316 continue 2; // Skip this tag, it doesn't have all the expected attributes
318 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
319 $allattributesmatch = false;
320 $this->failurereason = 'nomatch';
324 if ($allattributesmatch) {
325 $foundamatch = true;
327 // Now make sure this node doesn't have any of the forbidden attributes either
328 $nodeattrlist = $node->attributes;
330 foreach ($nodeattrlist as $domattrname => $domattr) {
331 if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) === (string) $this->forbiddenvalues[$domattrname]) {
332 $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
333 $foundamatch = false;
339 return $foundamatch;
342 function customMessage($html) {
343 $output = 'Content [' . $html . '] ';
345 if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
346 $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
347 } else if ($this->failurereason == 'nomatch') {
348 $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
349 foreach ($this->expectedvalues as $var => $val) {
350 $output .= "$var=\"$val\" ";
352 $output = rtrim($output);
353 $output .= '].';
356 return $output;
361 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
362 * All attributes must be present and their values must match the expected values.
363 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
365 * @copyright 2010 The Open University
366 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
368 class ContainsSelectExpectation extends XMLStructureExpectation {
370 * @var string $tag The name of the Tag to search
372 protected $name;
374 * @var array $expectedvalues An associative array of parameters, all of which must be matched
376 protected $choices;
378 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
380 protected $selected;
382 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
384 protected $enabled;
386 function __construct($name, $choices, $selected = null, $enabled = null, $message = '%s') {
387 parent::__construct($message);
388 $this->name = $name;
389 $this->choices = $choices;
390 $this->selected = $selected;
391 $this->enabled = $enabled;
394 function test($html) {
395 $parser = $this->load_xml($html);
396 if (is_array($parser)) {
397 return false;
400 $list = $parser->getElementsByTagName('select');
402 // Iterating through inputs
403 foreach ($list as $node) {
404 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
405 continue;
408 if ($node->getAttribute('name') != $this->name) {
409 continue;
412 if ($this->enabled === true && $node->getAttribute('disabled')) {
413 continue;
414 } else if ($this->enabled === false && $node->getAttribute('disabled') != 'disabled') {
415 continue;
418 $options = $node->getElementsByTagName('option');
419 reset($this->choices);
420 foreach ($options as $option) {
421 if ($option->getAttribute('value') != key($this->choices)) {
422 continue 2;
424 if ($option->firstChild->wholeText != current($this->choices)) {
425 continue 2;
427 if ($option->getAttribute('value') === $this->selected &&
428 !$option->hasAttribute('selected')) {
429 continue 2;
431 next($this->choices);
433 if (current($this->choices) !== false) {
434 // The HTML did not contain all the choices.
435 return false;
437 return true;
439 return false;
442 function customMessage($html) {
443 if ($this->enabled === true) {
444 $state = 'an enabled';
445 } else if ($this->enabled === false) {
446 $state = 'a disabled';
447 } else {
448 $state = 'a';
450 $output = 'Content [' . $html . '] does not contain ' . $state .
451 ' <select> with name ' . $this->name . ' and choices ' .
452 implode(', ', $this->choices);
453 if ($this->selected) {
454 $output .= ' with ' . $this->selected . ' selected).';
457 return $output;
462 * The opposite of {@link ContainsTagWithAttributes}. The test passes only if
463 * the HTML does not contain a tag with the given attributes.
465 * @copyright 2010 The Open University
466 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
468 class DoesNotContainTagWithAttributes extends ContainsTagWithAttributes {
469 function __construct($tag, $expectedvalues, $message = '%s') {
470 parent::__construct($tag, $expectedvalues, array(), $message);
472 function test($html) {
473 return !parent::test($html);
475 function customMessage($html) {
476 $output = 'Content [' . $html . '] ';
478 $output .= 'contains the tag [' . $this->tag . '] with attributes [';
479 foreach ($this->expectedvalues as $var => $val) {
480 $output .= "$var=\"$val\" ";
482 $output = rtrim($output);
483 $output .= '].';
485 return $output;
490 * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
492 * @copyright 2009 Tim Hunt
493 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
495 class ContainsTagWithContents extends XMLStructureExpectation {
496 protected $tag;
497 protected $content;
499 function __construct($tag, $content, $message = '%s') {
500 parent::__construct($message);
501 $this->tag = $tag;
502 $this->content = $content;
505 function test($html) {
506 $parser = $this->load_xml($html);
507 $list = $parser->getElementsByTagName($this->tag);
509 foreach ($list as $node) {
510 if ($node->textContent == $this->content) {
511 return true;
515 return false;
518 function testMessage($html) {
519 return 'Content [' . $html . '] does not contain the tag [' .
520 $this->tag . '] with contents [' . $this->content . '].';
525 * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
527 * @copyright 2009 Nicolas Connault
528 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
530 class ContainsEmptyTag extends XMLStructureExpectation {
531 protected $tag;
533 function __construct($tag, $message = '%s') {
534 parent::__construct($message);
535 $this->tag = $tag;
538 function test($html) {
539 $parser = $this->load_xml($html);
540 $list = $parser->getElementsByTagName($this->tag);
542 foreach ($list as $node) {
543 if (!$node->hasAttributes() && !$node->hasChildNodes()) {
544 return true;
548 return false;
551 function testMessage($html) {
552 return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
558 * Simple class that implements the {@link moodle_recordset} API based on an
559 * array of test data.
561 * See the {@link question_attempt_step_db_test} class in
562 * question/engine/simpletest/testquestionattemptstep.php for an example of how
563 * this is used.
565 * @copyright 2011 The Open University
566 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
568 class test_recordset extends moodle_recordset {
569 protected $records;
572 * Constructor
573 * @param $table as for {@link testing_db_record_builder::build_db_records()}
574 * but does not need a unique first column.
576 public function __construct(array $table) {
577 $columns = array_shift($table);
578 $this->records = array();
579 foreach ($table as $row) {
580 if (count($row) != count($columns)) {
581 throw new coding_exception("Row contains the wrong number of fields.");
583 $rec = array();
584 foreach ($columns as $i => $name) {
585 $rec[$name] = $row[$i];
587 $this->records[] = $rec;
589 reset($this->records);
592 public function __destruct() {
593 $this->close();
596 public function current() {
597 return (object) current($this->records);
600 public function key() {
601 if (is_null(key($this->records))) {
602 return false;
604 $current = current($this->records);
605 return reset($current);
608 public function next() {
609 next($this->records);
612 public function valid() {
613 return !is_null(key($this->records));
616 public function close() {
617 $this->records = null;
623 * This class lets you write unit tests that access a separate set of test
624 * tables with a different prefix. Only those tables you explicitly ask to
625 * be created will be.
627 * This class has failities for flipping $USER->id.
629 * The tear-down method for this class should automatically revert any changes
630 * you make during test set-up using the metods defined here. That is, it will
631 * drop tables for you automatically and revert to the real $DB and $USER->id.
633 * @package moodlecore
634 * @subpackage simpletestex
635 * @copyright &copy; 2006 The Open University
636 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
638 class UnitTestCaseUsingDatabase extends UnitTestCase {
639 private $realdb;
640 protected $testdb;
641 private $realuserid = null;
642 private $tables = array();
644 private $realcfg;
645 protected $testcfg;
647 public function __construct($label = false) {
648 global $DB, $CFG;
650 // Complain if we get this far and $CFG->unittestprefix is not set.
651 if (empty($CFG->unittestprefix)) {
652 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
655 // Only do this after the above text.
656 parent::UnitTestCase($label);
658 // Create the test DB instance.
659 $this->realdb = $DB;
660 $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
661 $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
663 // Set up test config
664 $this->testcfg = (object)array(
665 'testcfg' => true, // Marker that this is a test config
666 'libdir' => $CFG->libdir, // Must use real one so require_once works
667 'dirroot' => $CFG->dirroot, // Must use real one
668 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
669 'ostype' => $CFG->ostype, // Real one
670 'wwwroot' => 'http://www.example.org', // Use fixed url
671 'siteadmins' => '0', // No admins
672 'siteguest' => '0' // No guest
674 $this->realcfg = $CFG;
678 * Switch to using the test database for all queries until further notice.
680 protected function switch_to_test_db() {
681 global $DB;
682 if ($DB === $this->testdb) {
683 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);
685 $DB = $this->testdb;
689 * Revert to using the test database for all future queries.
691 protected function revert_to_real_db() {
692 global $DB;
693 if ($DB !== $this->testdb) {
694 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);
696 $DB = $this->realdb;
700 * Switch to using the test $CFG for all queries until further notice.
702 protected function switch_to_test_cfg() {
703 global $CFG;
704 if (isset($CFG->testcfg)) {
705 debugging('switch_to_test_cfg called when the test CFG was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
707 $CFG = $this->testcfg;
711 * Revert to using the real $CFG for all future queries.
713 protected function revert_to_real_cfg() {
714 global $CFG;
715 if (!isset($CFG->testcfg)) {
716 debugging('revert_to_real_cfg called when the test CFG was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
718 $CFG = $this->realcfg;
722 * Switch $USER->id to a test value.
724 * It might be worth making this method do more robuse $USER switching in future,
725 * however, this is sufficient for my needs at present.
727 protected function switch_global_user_id($userid) {
728 global $USER;
729 if (!is_null($this->realuserid)) {
730 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);
731 } else {
732 $this->realuserid = $USER->id;
734 $USER->id = $userid;
738 * Revert $USER->id to the real value.
740 protected function revert_global_user_id() {
741 global $USER;
742 if (is_null($this->realuserid)) {
743 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);
744 } else {
745 $USER->id = $this->realuserid;
746 $this->realuserid = null;
751 * Check that the user has not forgotten to clean anything up, and if they
752 * have, display a rude message and clean it up for them.
754 private function automatic_clean_up() {
755 global $DB, $CFG;
756 $cleanmore = false;
758 // Drop any test tables that were created.
759 foreach ($this->tables as $tablename => $notused) {
760 $this->drop_test_table($tablename);
763 // Switch back to the real DB if necessary.
764 if ($DB !== $this->realdb) {
765 $this->revert_to_real_db();
766 $cleanmore = true;
769 // Switch back to the real CFG if necessary.
770 if (isset($CFG->testcfg)) {
771 $this->revert_to_real_cfg();
772 $cleanmore = true;
775 // revert_global_user_id if necessary.
776 if (!is_null($this->realuserid)) {
777 $this->revert_global_user_id();
778 $cleanmore = true;
781 if ($cleanmore) {
782 accesslib_clear_all_caches_for_unit_testing();
783 $course = 'reset';
784 get_fast_modinfo($course);
788 public function tearDown() {
789 $this->automatic_clean_up();
790 parent::tearDown();
793 public function __destruct() {
794 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
795 $this->automatic_clean_up();
799 * Create a test table just like a real one, getting getting the definition from
800 * the specified install.xml file.
801 * @param string $tablename the name of the test table.
802 * @param string $installxmlfile the install.xml file in which this table is defined.
803 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
804 * so you need only specify, for example, 'mod/quiz'.
806 protected function create_test_table($tablename, $installxmlfile) {
807 global $CFG;
808 $dbman = $this->testdb->get_manager();
809 if (isset($this->tables[$tablename])) {
810 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
811 return;
813 if ($dbman->table_exists($tablename)) {
814 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);
815 $dbman->drop_table(new xmldb_table($tablename));
817 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
818 $this->tables[$tablename] = 1;
822 * Convenience method for calling create_test_table repeatedly.
823 * @param array $tablenames an array of table names.
824 * @param string $installxmlfile the install.xml file in which this table is defined.
825 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
826 * so you need only specify, for example, 'mod/quiz'.
828 protected function create_test_tables($tablenames, $installxmlfile) {
829 foreach ($tablenames as $tablename) {
830 $this->create_test_table($tablename, $installxmlfile);
835 * Drop a test table.
836 * @param $tablename the name of the test table.
838 protected function drop_test_table($tablename) {
839 if (!isset($this->tables[$tablename])) {
840 debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
841 return;
843 $dbman = $this->testdb->get_manager();
844 $table = new xmldb_table($tablename);
845 $dbman->drop_table($table);
846 unset($this->tables[$tablename]);
850 * Convenience method for calling drop_test_table repeatedly.
851 * @param array $tablenames an array of table names.
853 protected function drop_test_tables($tablenames) {
854 foreach ($tablenames as $tablename) {
855 $this->drop_test_table($tablename);
860 * Load a table with some rows of data. A typical call would look like:
862 * $config = $this->load_test_data('config_plugins',
863 * array('plugin', 'name', 'value'), array(
864 * array('frog', 'numlegs', 2),
865 * array('frog', 'sound', 'croak'),
866 * array('frog', 'action', 'jump'),
867 * ));
869 * @param string $table the table name.
870 * @param array $cols the columns to fill.
871 * @param array $data the data to load.
872 * @return array $objects corresponding to $data.
874 protected function load_test_data($table, array $cols, array $data) {
875 $results = array();
876 foreach ($data as $rowid => $row) {
877 $obj = new stdClass;
878 foreach ($cols as $key => $colname) {
879 $obj->$colname = $row[$key];
881 $obj->id = $this->testdb->insert_record($table, $obj);
882 $results[$rowid] = $obj;
884 return $results;
888 * Clean up data loaded with load_test_data. The call corresponding to the
889 * example load above would be:
891 * $this->delete_test_data('config_plugins', $config);
893 * @param string $table the table name.
894 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
896 protected function delete_test_data($table, array $rows) {
897 $ids = array();
898 foreach ($rows as $row) {
899 $ids[] = $row->id;
901 $this->testdb->delete_records_list($table, 'id', $ids);
907 * @package moodlecore
908 * @subpackage simpletestex
909 * @copyright &copy; 2006 The Open University
910 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
912 class FakeDBUnitTestCase extends UnitTestCase {
913 public $tables = array();
914 public $pkfile;
915 public $cfg;
916 public $DB;
919 * In the constructor, record the max(id) of each test table into a csv file.
920 * If this file already exists, it means that a previous run of unit tests
921 * did not complete, and has left data undeleted in the DB. This data is then
922 * deleted and the file is retained. Otherwise it is created.
924 * throws moodle_exception if CSV file cannot be created
926 public function __construct($label = false) {
927 global $DB, $CFG;
929 if (empty($CFG->unittestprefix)) {
930 return;
933 parent::UnitTestCase($label);
934 // MDL-16483 Get PKs and save data to text file
936 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
937 $this->cfg = $CFG;
939 UnitTestDB::instantiate();
941 $tables = $DB->get_tables();
943 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
944 if (file_exists($this->pkfile)) {
945 $this->truncate_test_tables($this->get_table_data($this->pkfile));
947 } else { // Create the file
948 $tabledata = '';
950 foreach ($tables as $table) {
951 if ($table != 'sessions') {
952 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
953 $max_id = 0;
955 $tabledata .= "$table, $max_id\n";
958 if (!file_put_contents($this->pkfile, $tabledata)) {
959 $a = new stdClass();
960 $a->filename = $this->pkfile;
961 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
967 * 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.
968 * @param array $tabledata
970 private function truncate_test_tables($tabledata) {
971 global $CFG, $DB;
973 if (empty($CFG->unittestprefix)) {
974 return;
977 $tables = $DB->get_tables();
979 foreach ($tables as $table) {
980 if ($table != 'sessions' && isset($tabledata[$table])) {
981 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
987 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
988 * 1. Table name
989 * 2. Max id
991 * throws moodle_exception if file doesn't exist
993 * @param string $filename
995 public function get_table_data($filename) {
996 global $CFG;
998 if (empty($CFG->unittestprefix)) {
999 return;
1002 if (file_exists($this->pkfile)) {
1003 $handle = fopen($this->pkfile, 'r');
1004 $tabledata = array();
1006 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
1007 $tabledata[$data[0]] = $data[1];
1009 return $tabledata;
1010 } else {
1011 $a = new stdClass();
1012 $a->filename = $this->pkfile;
1013 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
1014 return false;
1019 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
1020 * Also detects if this config setting is properly set, and if the user table exists.
1021 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
1023 public function setUp() {
1024 global $DB, $CFG;
1026 if (empty($CFG->unittestprefix)) {
1027 return;
1030 parent::setUp();
1031 $this->DB =& $DB;
1032 ob_start();
1036 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
1038 public function tearDown() {
1039 global $DB, $CFG;
1041 if (empty($CFG->unittestprefix)) {
1042 return;
1045 if (empty($DB)) {
1046 $DB = $this->DB;
1048 $DB->cleanup();
1049 parent::tearDown();
1051 // Output buffering
1052 if (ob_get_length() > 0) {
1053 ob_end_flush();
1058 * 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
1059 * It should also detect if data is missing from the original tables.
1061 public function __destruct() {
1062 global $CFG, $DB;
1064 if (empty($CFG->unittestprefix)) {
1065 return;
1068 $CFG = $this->cfg;
1069 $this->tearDown();
1070 UnitTestDB::restore();
1071 fulldelete($this->pkfile);
1075 * Load a table with some rows of data. A typical call would look like:
1077 * $config = $this->load_test_data('config_plugins',
1078 * array('plugin', 'name', 'value'), array(
1079 * array('frog', 'numlegs', 2),
1080 * array('frog', 'sound', 'croak'),
1081 * array('frog', 'action', 'jump'),
1082 * ));
1084 * @param string $table the table name.
1085 * @param array $cols the columns to fill.
1086 * @param array $data the data to load.
1087 * @return array $objects corresponding to $data.
1089 public function load_test_data($table, array $cols, array $data) {
1090 global $CFG, $DB;
1092 if (empty($CFG->unittestprefix)) {
1093 return;
1096 $results = array();
1097 foreach ($data as $rowid => $row) {
1098 $obj = new stdClass;
1099 foreach ($cols as $key => $colname) {
1100 $obj->$colname = $row[$key];
1102 $obj->id = $DB->insert_record($table, $obj);
1103 $results[$rowid] = $obj;
1105 return $results;
1109 * Clean up data loaded with load_test_data. The call corresponding to the
1110 * example load above would be:
1112 * $this->delete_test_data('config_plugins', $config);
1114 * @param string $table the table name.
1115 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
1117 public function delete_test_data($table, array $rows) {
1118 global $CFG, $DB;
1120 if (empty($CFG->unittestprefix)) {
1121 return;
1124 $ids = array();
1125 foreach ($rows as $row) {
1126 $ids[] = $row->id;
1128 $DB->delete_records_list($table, 'id', $ids);
1133 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
1134 * static instantiate() method, and restores the original global $DB through restore().
1135 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
1136 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
1137 * without subclassing it.
1139 * @package moodlecore
1140 * @subpackage simpletestex
1141 * @copyright &copy; 2006 The Open University
1142 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1144 class UnitTestDB {
1145 public static $DB;
1146 private static $real_db;
1148 public $table_data = array();
1151 * Call this statically to connect to the DB using the unittest prefix, instantiate
1152 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
1154 public static function instantiate() {
1155 global $CFG, $DB;
1156 UnitTestDB::$real_db = clone($DB);
1157 if (empty($CFG->unittestprefix)) {
1158 print_error("prefixnotset", 'simpletest');
1161 if (empty(UnitTestDB::$DB)) {
1162 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
1163 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
1166 $manager = UnitTestDB::$DB->get_manager();
1168 if (!$manager->table_exists('user')) {
1169 print_error('tablesnotsetup', 'simpletest');
1172 $DB = new UnitTestDB();
1175 public function __call($method, $args) {
1176 // Set args to null if they don't exist (up to 10 args should do)
1177 if (!method_exists($this, $method)) {
1178 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
1179 } else {
1180 call_user_func_array(array($this, $method), $args);
1184 public function __get($variable) {
1185 return UnitTestDB::$DB->$variable;
1188 public function __set($variable, $value) {
1189 UnitTestDB::$DB->$variable = $value;
1192 public function __isset($variable) {
1193 return isset(UnitTestDB::$DB->$variable);
1196 public function __unset($variable) {
1197 unset(UnitTestDB::$DB->$variable);
1201 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
1203 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1204 global $DB;
1205 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
1206 $this->table_data[$table][] = $id;
1207 return $id;
1211 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
1212 * throw an exception and cancel update.
1214 * throws moodle_exception If trying to update a record not inserted by unit tests.
1216 public function update_record($table, $dataobject, $bulk=false) {
1217 global $DB;
1218 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
1219 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1220 $a = new stdClass();
1221 $a->id = $dataobject->id;
1222 $a->table = $table;
1223 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
1224 } else {
1225 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1230 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1231 * throw an exception and cancel delete.
1233 * throws moodle_exception If trying to delete a record not inserted by unit tests.
1235 public function delete_records($table, array $conditions=array()) {
1236 global $DB;
1237 $tables_to_ignore = array('context_temp');
1239 $a = new stdClass();
1240 $a->table = $table;
1242 // Get ids matching conditions
1243 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
1244 return UnitTestDB::$DB->delete_records($table, $conditions);
1247 $proceed_with_delete = true;
1249 if (!is_array($ids_to_delete)) {
1250 $ids_to_delete = array($ids_to_delete);
1253 foreach ($ids_to_delete as $id) {
1254 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
1255 $proceed_with_delete = false;
1256 $a->id = $id;
1257 break;
1261 if ($proceed_with_delete) {
1262 return UnitTestDB::$DB->delete_records($table, $conditions);
1263 } else {
1264 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1269 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1270 * throw an exception and cancel delete.
1272 * throws moodle_exception If trying to delete a record not inserted by unit tests.
1274 public function delete_records_select($table, $select, array $params=null) {
1275 global $DB;
1276 $a = new stdClass();
1277 $a->table = $table;
1279 // Get ids matching conditions
1280 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1281 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1284 $proceed_with_delete = true;
1286 foreach ($ids_to_delete as $id) {
1287 if (!in_array($id, $this->table_data[$table])) {
1288 $proceed_with_delete = false;
1289 $a->id = $id;
1290 break;
1294 if ($proceed_with_delete) {
1295 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1296 } else {
1297 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1302 * Removes from the test DB all the records that were inserted during unit tests,
1304 public function cleanup() {
1305 global $DB;
1306 foreach ($this->table_data as $table => $ids) {
1307 foreach ($ids as $id) {
1308 $DB->delete_records($table, array('id' => $id));
1314 * Restores the global $DB object.
1316 public static function restore() {
1317 global $DB;
1318 $DB = UnitTestDB::$real_db;
1321 public function get_field($table, $return, array $conditions) {
1322 if (!is_array($conditions)) {
1323 throw new coding_exception('$conditions is not an array.');
1325 return UnitTestDB::$DB->get_field($table, $return, $conditions);