33a1975d76feede7b3461f157e49c46aaaa7137b
[moodle.git] / lib / simpletestlib.php
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.
14 //
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.
20  *
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.
24  *
25  * Major Contirbutors
26  *     - T.J.Hunt@open.ac.uk
27  *
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
32  */
34 defined('MOODLE_INTERNAL') || die();
36 /**
37  * Includes
38  */
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.
50  *
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).
57  */
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);
69             }
70         } else if ($exclude xor preg_match($fileregexp, $filepath)) {
71             call_user_func($callback, $filepath);
72         }
73     }
74 }
76 /**
77  * An expectation for comparing strings ignoring whitespace.
78  *
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
83  */
84 class IgnoreWhitespaceExpectation extends SimpleExpectation {
85     var $expect;
87     function IgnoreWhitespaceExpectation($content, $message = '%s') {
88         $this->SimpleExpectation($message);
89         $this->expect=$this->normalise($content);
90     }
92     function test($ip) {
93         return $this->normalise($ip)==$this->expect;
94     }
96     function normalise($text) {
97         return preg_replace('/\s+/m',' ',trim($text));
98     }
100     function testMessage($ip) {
101         return "Input string [$ip] doesn't match the required value.";
102     }
105 /**
106  * An Expectation that two arrays contain the same list of values.
107  *
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
112  */
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.');
121         }
122         $this->expect = $this->normalise($expected);
123     }
125     function test($actual) {
126         return $this->normalise($actual) == $this->expect;
127     }
129     function normalise($array) {
130         sort($array);
131         return $array;
132     }
134     function testMessage($actual) {
135         return 'Array [' . implode(', ', $actual) .
136                 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
137     }
141 /**
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.)
145  *
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
150  */
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.');
159         }
160         $this->expect = $expected;
161     }
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;
171             }
172         }
173         return true;
174     }
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 . '].';
185             }
186         }
187         return 'Actual object does not have all the same fields with the same values as the expected object (' .
188                 implode(', ', $mismatches) . ').';
189     }
192 abstract class XMLStructureExpectation extends SimpleExpectation {
193     /**
194      * Parse a string as XML and return a DOMDocument;
195      * @param $html
196      * @return unknown_type
197      */
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();
203         }
204         libxml_clear_errors();
205         libxml_use_internal_errors($prevsetting);
206         return $parser;
207     }
209 /**
210  * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
211  *
212  * @copyright 2009 Tim Hunt
213  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
214  */
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;
225     }
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 === (string) $this->value) {
233                 return true;
234             }
235         }
236         return false;
237     }
239     function testMessage($html) {
240         return 'Content [' . $html . '] does not contain the tag [' .
241                 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
242     }
245 /**
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.
249  *
250  * @copyright 2009 Nicolas Connault
251  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
252  */
253 class ContainsTagWithAttributes extends XMLStructureExpectation {
254     /**
255      * @var string $tag The name of the Tag to search
256      */
257     protected $tag;
258     /**
259      * @var array $expectedvalues An associative array of parameters, all of which must be matched
260      */
261     protected $expectedvalues = array();
262     /**
263      * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
264      */
265     protected $forbiddenvalues = array();
266     /**
267      * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
268      */
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;
276     }
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;
288             }
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
297                 }
298                 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
299                     $allattributesmatch = false;
300                     $this->failurereason = 'nomatch';
301                 }
302             }
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) === (string) $this->forbiddenvalues[$domattrname]) {
312                         $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
313                         $foundamatch = false;
314                     }
315                 }
316             }
317         }
319         return $foundamatch;
320     }
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\" ";
331             }
332             $output = rtrim($output);
333             $output .= '].';
334         }
336         return $output;
337     }
340 /**
341  * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
342  *
343  * @copyright 2009 Tim Hunt
344  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
345  */
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;
354     }
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;
363             }
364         }
366         return false;
367     }
369     function testMessage($html) {
370         return 'Content [' . $html . '] does not contain the tag [' .
371                 $this->tag . '] with contents [' . $this->content . '].';
372     }
375 /**
376  * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
377  *
378  * @copyright 2009 Nicolas Connault
379  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
380  */
381 class ContainsEmptyTag extends XMLStructureExpectation {
382     protected $tag;
384     function __construct($tag, $message = '%s') {
385         parent::__construct($message);
386         $this->tag = $tag;
387     }
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;
396             }
397         }
399         return false;
400     }
402     function testMessage($html) {
403         return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
404     }
408 /**
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.
412  *
413  * This class has failities for flipping $USER->id.
414  *
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.
418  *
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
423  */
424 class UnitTestCaseUsingDatabase extends UnitTestCase {
425     private $realdb;
426     protected $testdb;
427     private $realuserid = null;
428     private $tables = array();
430     private $realcfg;
431     protected $testcfg;
433     public function __construct($label = false) {
434         global $DB, $CFG;
436         // Complain if we get this far and $CFG->unittestprefix is not set.
437         if (empty($CFG->unittestprefix)) {
438             throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
439         }
441         // Only do this after the above text.
442         parent::UnitTestCase($label);
444         // Create the test DB instance.
445         $this->realdb = $DB;
446         $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
447         $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
449         // Set up test config
450         $this->testcfg = (object)array(
451                 'testcfg' => true, // Marker that this is a test config
452                 'libdir' => $CFG->libdir, // Must use real one so require_once works
453                 'dirroot' => $CFG->dirroot, // Must use real one
454                 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
455                 'ostype' => $CFG->ostype, // Real one
456                 'wwwroot' => 'http://www.example.org', // Use fixed url
457                 'siteadmins' => '0', // No admins
458                 'siteguest' => '0' // No guest
459         );
460         $this->realcfg = $CFG;
461     }
463     /**
464      * Switch to using the test database for all queries until further notice.
465      */
466     protected function switch_to_test_db() {
467         global $DB;
468         if ($DB === $this->testdb) {
469             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);
470         }
471         $DB = $this->testdb;
472     }
474     /**
475      * Revert to using the test database for all future queries.
476      */
477     protected function revert_to_real_db() {
478         global $DB;
479         if ($DB !== $this->testdb) {
480             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);
481         }
482         $DB = $this->realdb;
483     }
485     /**
486      * Switch to using the test $CFG for all queries until further notice.
487      */
488     protected function switch_to_test_cfg() {
489         global $CFG;
490         if (isset($CFG->testcfg)) {
491             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);
492         }
493         $CFG = $this->testcfg;
494     }
496     /**
497      * Revert to using the real $CFG for all future queries.
498      */
499     protected function revert_to_real_cfg() {
500         global $CFG;
501         if (!isset($CFG->testcfg)) {
502             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);
503         }
504         $CFG = $this->realcfg;
505     }
507     /**
508      * Switch $USER->id to a test value.
509      *
510      * It might be worth making this method do more robuse $USER switching in future,
511      * however, this is sufficient for my needs at present.
512      */
513     protected function switch_global_user_id($userid) {
514         global $USER;
515         if (!is_null($this->realuserid)) {
516             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);
517         } else {
518             $this->realuserid = $USER->id;
519         }
520         $USER->id = $userid;
521     }
523     /**
524      * Revert $USER->id to the real value.
525      */
526     protected function revert_global_user_id() {
527         global $USER;
528         if (is_null($this->realuserid)) {
529             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);
530         } else {
531             $USER->id = $this->realuserid;
532             $this->realuserid = null;
533         }
534     }
536     /**
537      * Check that the user has not forgotten to clean anything up, and if they
538      * have, display a rude message and clean it up for them.
539      */
540     private function automatic_clean_up() {
541         global $DB, $CFG;
542         $cleanmore = false;
544         // Drop any test tables that were created.
545         foreach ($this->tables as $tablename => $notused) {
546             $this->drop_test_table($tablename);
547         }
549         // Switch back to the real DB if necessary.
550         if ($DB !== $this->realdb) {
551             $this->revert_to_real_db();
552             $cleanmore = true;
553         }
555         // Switch back to the real CFG if necessary.
556         if (isset($CFG->testcfg)) {
557             $this->revert_to_real_cfg();
558             $cleanmore = true;
559         }
561         // revert_global_user_id if necessary.
562         if (!is_null($this->realuserid)) {
563             $this->revert_global_user_id();
564             $cleanmore = true;
565         }
567         if ($cleanmore) {
568             accesslib_clear_all_caches_for_unit_testing();
569         }
570     }
572     public function tearDown() {
573         $this->automatic_clean_up();
574         parent::tearDown();
575     }
577     public function __destruct() {
578         // Should not be necessary thanks to tearDown, but no harm in belt and braces.
579         $this->automatic_clean_up();
580     }
582     /**
583      * Create a test table just like a real one, getting getting the definition from
584      * the specified install.xml file.
585      * @param string $tablename the name of the test table.
586      * @param string $installxmlfile the install.xml file in which this table is defined.
587      *      $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
588      *      so you need only specify, for example, 'mod/quiz'.
589      */
590     protected function create_test_table($tablename, $installxmlfile) {
591         global $CFG;
592         $dbman = $this->testdb->get_manager();
593         if (isset($this->tables[$tablename])) {
594             debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
595             return;
596         }
597         if ($dbman->table_exists($tablename)) {
598             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);
599             $dbman->drop_table(new xmldb_table($tablename));
600         }
601         $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
602         $this->tables[$tablename] = 1;
603     }
605     /**
606      * Convenience method for calling create_test_table repeatedly.
607      * @param array $tablenames an array of table names.
608      * @param string $installxmlfile the install.xml file in which this table is defined.
609      *      $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
610      *      so you need only specify, for example, 'mod/quiz'.
611      */
612     protected function create_test_tables($tablenames, $installxmlfile) {
613         foreach ($tablenames as $tablename) {
614             $this->create_test_table($tablename, $installxmlfile);
615         }
616     }
618     /**
619      * Drop a test table.
620      * @param $tablename the name of the test table.
621      */
622     protected function drop_test_table($tablename) {
623         if (!isset($this->tables[$tablename])) {
624             debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
625             return;
626         }
627         $dbman = $this->testdb->get_manager();
628         $table = new xmldb_table($tablename);
629         $dbman->drop_table($table);
630         unset($this->tables[$tablename]);
631     }
633     /**
634      * Convenience method for calling drop_test_table repeatedly.
635      * @param array $tablenames an array of table names.
636      */
637     protected function drop_test_tables($tablenames) {
638         foreach ($tablenames as $tablename) {
639             $this->drop_test_table($tablename);
640         }
641     }
643     /**
644      * Load a table with some rows of data. A typical call would look like:
645      *
646      * $config = $this->load_test_data('config_plugins',
647      *         array('plugin', 'name', 'value'), array(
648      *         array('frog', 'numlegs', 2),
649      *         array('frog', 'sound', 'croak'),
650      *         array('frog', 'action', 'jump'),
651      * ));
652      *
653      * @param string $table the table name.
654      * @param array $cols the columns to fill.
655      * @param array $data the data to load.
656      * @return array $objects corresponding to $data.
657      */
658     protected function load_test_data($table, array $cols, array $data) {
659         $results = array();
660         foreach ($data as $rowid => $row) {
661             $obj = new stdClass;
662             foreach ($cols as $key => $colname) {
663                 $obj->$colname = $row[$key];
664             }
665             $obj->id = $this->testdb->insert_record($table, $obj);
666             $results[$rowid] = $obj;
667         }
668         return $results;
669     }
671     /**
672      * Clean up data loaded with load_test_data. The call corresponding to the
673      * example load above would be:
674      *
675      * $this->delete_test_data('config_plugins', $config);
676      *
677      * @param string $table the table name.
678      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
679      */
680     protected function delete_test_data($table, array $rows) {
681         $ids = array();
682         foreach ($rows as $row) {
683             $ids[] = $row->id;
684         }
685         $this->testdb->delete_records_list($table, 'id', $ids);
686     }
690 /**
691  * @package moodlecore
692  * @subpackage simpletestex
693  * @copyright &copy; 2006 The Open University
694  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
695  */
696 class FakeDBUnitTestCase extends UnitTestCase {
697     public $tables = array();
698     public $pkfile;
699     public $cfg;
700     public $DB;
702     /**
703      * In the constructor, record the max(id) of each test table into a csv file.
704      * If this file already exists, it means that a previous run of unit tests
705      * did not complete, and has left data undeleted in the DB. This data is then
706      * deleted and the file is retained. Otherwise it is created.
707      *
708      * throws moodle_exception if CSV file cannot be created
709      */
710     public function __construct($label = false) {
711         global $DB, $CFG;
713         if (empty($CFG->unittestprefix)) {
714             return;
715         }
717         parent::UnitTestCase($label);
718         // MDL-16483 Get PKs and save data to text file
720         $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
721         $this->cfg = $CFG;
723         UnitTestDB::instantiate();
725         $tables = $DB->get_tables();
727         // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
728         if (file_exists($this->pkfile)) {
729             $this->truncate_test_tables($this->get_table_data($this->pkfile));
731         } else { // Create the file
732             $tabledata = '';
734             foreach ($tables as $table) {
735                 if ($table != 'sessions') {
736                     if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
737                         $max_id = 0;
738                     }
739                     $tabledata .= "$table, $max_id\n";
740                 }
741             }
742             if (!file_put_contents($this->pkfile, $tabledata)) {
743                 $a = new stdClass();
744                 $a->filename = $this->pkfile;
745                 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
746             }
747         }
748     }
750     /**
751      * 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.
752      * @param array $tabledata
753      */
754     private function truncate_test_tables($tabledata) {
755         global $CFG, $DB;
757         if (empty($CFG->unittestprefix)) {
758             return;
759         }
761         $tables = $DB->get_tables();
763         foreach ($tables as $table) {
764             if ($table != 'sessions' && isset($tabledata[$table])) {
765                 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
766             }
767         }
768     }
770     /**
771      * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
772      * 1. Table name
773      * 2. Max id
774      *
775      * throws moodle_exception if file doesn't exist
776      *
777      * @param string $filename
778      */
779     public function get_table_data($filename) {
780         global $CFG;
782         if (empty($CFG->unittestprefix)) {
783             return;
784         }
786         if (file_exists($this->pkfile)) {
787             $handle = fopen($this->pkfile, 'r');
788             $tabledata = array();
790             while (($data = fgetcsv($handle, 1000, ",")) !== false) {
791                 $tabledata[$data[0]] = $data[1];
792             }
793             return $tabledata;
794         } else {
795             $a = new stdClass();
796             $a->filename = $this->pkfile;
797             throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
798             return false;
799         }
800     }
802     /**
803      * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
804      * Also detects if this config setting is properly set, and if the user table exists.
805      * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
806      */
807     public function setUp() {
808         global $DB, $CFG;
810         if (empty($CFG->unittestprefix)) {
811             return;
812         }
814         parent::setUp();
815         $this->DB =& $DB;
816         ob_start();
817     }
819     /**
820      * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
821      */
822     public function tearDown() {
823         global $DB, $CFG;
825         if (empty($CFG->unittestprefix)) {
826             return;
827         }
829         if (empty($DB)) {
830             $DB = $this->DB;
831         }
832         $DB->cleanup();
833         parent::tearDown();
835         // Output buffering
836         if (ob_get_length() > 0) {
837             ob_end_flush();
838         }
839     }
841     /**
842      * 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
843      * It should also detect if data is missing from the original tables.
844      */
845     public function __destruct() {
846         global $CFG, $DB;
848         if (empty($CFG->unittestprefix)) {
849             return;
850         }
852         $CFG = $this->cfg;
853         $this->tearDown();
854         UnitTestDB::restore();
855         fulldelete($this->pkfile);
856     }
858     /**
859      * Load a table with some rows of data. A typical call would look like:
860      *
861      * $config = $this->load_test_data('config_plugins',
862      *         array('plugin', 'name', 'value'), array(
863      *         array('frog', 'numlegs', 2),
864      *         array('frog', 'sound', 'croak'),
865      *         array('frog', 'action', 'jump'),
866      * ));
867      *
868      * @param string $table the table name.
869      * @param array $cols the columns to fill.
870      * @param array $data the data to load.
871      * @return array $objects corresponding to $data.
872      */
873     public function load_test_data($table, array $cols, array $data) {
874         global $CFG, $DB;
876         if (empty($CFG->unittestprefix)) {
877             return;
878         }
880         $results = array();
881         foreach ($data as $rowid => $row) {
882             $obj = new stdClass;
883             foreach ($cols as $key => $colname) {
884                 $obj->$colname = $row[$key];
885             }
886             $obj->id = $DB->insert_record($table, $obj);
887             $results[$rowid] = $obj;
888         }
889         return $results;
890     }
892     /**
893      * Clean up data loaded with load_test_data. The call corresponding to the
894      * example load above would be:
895      *
896      * $this->delete_test_data('config_plugins', $config);
897      *
898      * @param string $table the table name.
899      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
900      */
901     public function delete_test_data($table, array $rows) {
902         global $CFG, $DB;
904         if (empty($CFG->unittestprefix)) {
905             return;
906         }
908         $ids = array();
909         foreach ($rows as $row) {
910             $ids[] = $row->id;
911         }
912         $DB->delete_records_list($table, 'id', $ids);
913     }
916 /**
917  * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
918  * static instantiate() method, and restores the original global $DB through restore().
919  * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
920  * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
921  * without subclassing it.
922  *
923  * @package moodlecore
924  * @subpackage simpletestex
925  * @copyright &copy; 2006 The Open University
926  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
927  */
928 class UnitTestDB {
929     public static $DB;
930     private static $real_db;
932     public $table_data = array();
934     /**
935      * Call this statically to connect to the DB using the unittest prefix, instantiate
936      * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
937      */
938     public static function instantiate() {
939         global $CFG, $DB;
940         UnitTestDB::$real_db = clone($DB);
941         if (empty($CFG->unittestprefix)) {
942             print_error("prefixnotset", 'simpletest');
943         }
945         if (empty(UnitTestDB::$DB)) {
946             UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
947             UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
948         }
950         $manager = UnitTestDB::$DB->get_manager();
952         if (!$manager->table_exists('user')) {
953             print_error('tablesnotsetup', 'simpletest');
954         }
956         $DB = new UnitTestDB();
957     }
959     public function __call($method, $args) {
960         // Set args to null if they don't exist (up to 10 args should do)
961         if (!method_exists($this, $method)) {
962             return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
963         } else {
964             call_user_func_array(array($this, $method), $args);
965         }
966     }
968     public function __get($variable) {
969         return UnitTestDB::$DB->$variable;
970     }
972     public function __set($variable, $value) {
973         UnitTestDB::$DB->$variable = $value;
974     }
976     public function __isset($variable) {
977         return isset(UnitTestDB::$DB->$variable);
978     }
980     public function __unset($variable) {
981         unset(UnitTestDB::$DB->$variable);
982     }
984     /**
985      * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
986      */
987     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
988         global $DB;
989         $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
990         $this->table_data[$table][] = $id;
991         return $id;
992     }
994     /**
995      * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
996      * throw an exception and cancel update.
997      *
998      * throws moodle_exception If trying to update a record not inserted by unit tests.
999      */
1000     public function update_record($table, $dataobject, $bulk=false) {
1001         global $DB;
1002         if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
1003             // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1004             $a = new stdClass();
1005             $a->id = $dataobject->id;
1006             $a->table = $table;
1007             throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
1008         } else {
1009             return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1010         }
1011     }
1013     /**
1014      * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1015      * throw an exception and cancel delete.
1016      *
1017      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1018      */
1019     public function delete_records($table, array $conditions=array()) {
1020         global $DB;
1021         $tables_to_ignore = array('context_temp');
1023         $a = new stdClass();
1024         $a->table = $table;
1026         // Get ids matching conditions
1027         if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
1028             return UnitTestDB::$DB->delete_records($table, $conditions);
1029         }
1031         $proceed_with_delete = true;
1033         if (!is_array($ids_to_delete)) {
1034             $ids_to_delete = array($ids_to_delete);
1035         }
1037         foreach ($ids_to_delete as $id) {
1038             if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
1039                 $proceed_with_delete = false;
1040                 $a->id = $id;
1041                 break;
1042             }
1043         }
1045         if ($proceed_with_delete) {
1046             return UnitTestDB::$DB->delete_records($table, $conditions);
1047         } else {
1048             throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1049         }
1050     }
1052     /**
1053      * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1054      * throw an exception and cancel delete.
1055      *
1056      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1057      */
1058     public function delete_records_select($table, $select, array $params=null) {
1059         global $DB;
1060         $a = new stdClass();
1061         $a->table = $table;
1063         // Get ids matching conditions
1064         if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1065             return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1066         }
1068         $proceed_with_delete = true;
1070         foreach ($ids_to_delete as $id) {
1071             if (!in_array($id, $this->table_data[$table])) {
1072                 $proceed_with_delete = false;
1073                 $a->id = $id;
1074                 break;
1075             }
1076         }
1078         if ($proceed_with_delete) {
1079             return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1080         } else {
1081             throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1082         }
1083     }
1085     /**
1086      * Removes from the test DB all the records that were inserted during unit tests,
1087      */
1088     public function cleanup() {
1089         global $DB;
1090         foreach ($this->table_data as $table => $ids) {
1091             foreach ($ids as $id) {
1092                 $DB->delete_records($table, array('id' => $id));
1093             }
1094         }
1095     }
1097     /**
1098      * Restores the global $DB object.
1099      */
1100     public static function restore() {
1101         global $DB;
1102         $DB = UnitTestDB::$real_db;
1103     }
1105     public function get_field($table, $return, array $conditions) {
1106         if (!is_array($conditions)) {
1107             throw new coding_exception('$conditions is not an array.');
1108         }
1109         return UnitTestDB::$DB->get_field($table, $return, $conditions);
1110     }