12b1eff2feef1319b2d16bc25c69266fba46e6aa
[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 if (!isset($actual->$key)) {
184                 $mismatches[] = $key . ' (expected [' . $value . '] but was missing.';
185             } else {
186                 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
187             }
188         }
189         return 'Actual object does not have all the same fields with the same values as the expected object (' .
190                 implode(', ', $mismatches) . ').';
191     }
194 abstract class XMLStructureExpectation extends SimpleExpectation {
195     /**
196      * Parse a string as XML and return a DOMDocument;
197      * @param $html
198      * @return unknown_type
199      */
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();
205         }
206         libxml_clear_errors();
207         libxml_use_internal_errors($prevsetting);
208         return $parser;
209     }
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;
216             }
217             return 'Could not parse XML [' . $html . '] errors were [' . 
218                     implode('], [', $parsererrors) . ']';
219         }
220         return $this->customMessage($html);
221     }
223 /**
224  * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
225  *
226  * @copyright 2009 Tim Hunt
227  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
228  */
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;
239     }
241     function test($html) {
242         $parser = $this->load_xml($html);
243         if (is_array($parser)) {
244             return false;
245         }
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;
251             }
252         }
253         return false;
254     }
256     function customMessage($html) {
257         return 'Content [' . $html . '] does not contain the tag [' .
258                 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
259     }
262 /**
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.
266  *
267  * @copyright 2009 Nicolas Connault
268  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
269  */
270 class ContainsTagWithAttributes extends XMLStructureExpectation {
271     /**
272      * @var string $tag The name of the Tag to search
273      */
274     protected $tag;
275     /**
276      * @var array $expectedvalues An associative array of parameters, all of which must be matched
277      */
278     protected $expectedvalues = array();
279     /**
280      * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
281      */
282     protected $forbiddenvalues = array();
283     /**
284      * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
285      */
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;
293     }
295     function test($html) {
296         $parser = $this->load_xml($html);
297         if (is_array($parser)) {
298             return false;
299         }
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;
308             }
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
317                 }
318                 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
319                     $allattributesmatch = false;
320                     $this->failurereason = 'nomatch';
321                 }
322             }
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;
334                     }
335                 }
336             }
337         }
339         return $foundamatch;
340     }
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\" ";
351             }
352             $output = rtrim($output);
353             $output .= '].';
354         }
356         return $output;
357     }
360 /**
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.
364  *
365  * @copyright 2010 The Open University
366  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
367  */
368 class ContainsSelectExpectation extends XMLStructureExpectation {
369     /**
370      * @var string $tag The name of the Tag to search
371      */
372     protected $name;
373     /**
374      * @var array $expectedvalues An associative array of parameters, all of which must be matched
375      */
376     protected $choices;
377     /**
378      * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
379      */
380     protected $selected;
381     /**
382      * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
383      */
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;
392     }
394     function test($html) {
395         $parser = $this->load_xml($html);
396         if (is_array($parser)) {
397             return false;
398         }
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;
406             }
408             if ($node->getAttribute('name') != $this->name) {
409                 continue;
410             }
412             if ($this->enabled === true && $node->getAttribute('disabled')) {
413                 continue;
414             } else if ($this->enabled === false && $node->getAttribute('disabled') != 'disabled') {
415                 continue;
416             }
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;
423                 }
424                 if ($option->firstChild->wholeText != current($this->choices)) {
425                     continue 2;
426                 }
427                 if ($option->getAttribute('value') === $this->selected &&
428                         !$option->hasAttribute('selected')) {
429                     continue 2;
430                 }
431                 next($this->choices);
432             }
433             if (current($this->choices) !== false) {
434                 // The HTML did not contain all the choices.
435                 return false;
436             }
437             return true;
438         }
439         return false;
440     }
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';
449         }
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).';
455         }
457         return $output;
458     }
461 /**
462  * The opposite of {@link ContainsTagWithAttributes}. The test passes only if
463  * the HTML does not contain a tag with the given attributes.
464  *
465  * @copyright 2010 The Open University
466  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
467  */
468 class DoesNotContainTagWithAttributes extends ContainsTagWithAttributes {
469     function __construct($tag, $expectedvalues, $message = '%s') {
470         parent::__construct($tag, $expectedvalues, array(), $message);
471     }
472     function test($html) {
473         return !parent::test($html);
474     }
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\" ";
481         }
482         $output = rtrim($output);
483         $output .= '].';
485         return $output;
486     }
489 /**
490  * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
491  *
492  * @copyright 2009 Tim Hunt
493  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
494  */
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;
503     }
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;
512             }
513         }
515         return false;
516     }
518     function testMessage($html) {
519         return 'Content [' . $html . '] does not contain the tag [' .
520                 $this->tag . '] with contents [' . $this->content . '].';
521     }
524 /**
525  * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
526  *
527  * @copyright 2009 Nicolas Connault
528  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
529  */
530 class ContainsEmptyTag extends XMLStructureExpectation {
531     protected $tag;
533     function __construct($tag, $message = '%s') {
534         parent::__construct($message);
535         $this->tag = $tag;
536     }
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;
545             }
546         }
548         return false;
549     }
551     function testMessage($html) {
552         return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
553     }
557 /**
558  * Simple class that implements the {@link moodle_recordset} API based on an
559  * array of test data.
560  *
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.
564  *
565  * @copyright  2011 The Open University
566  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
567  */
568 class test_recordset extends moodle_recordset {
569     protected $records;
571     /**
572      * Constructor
573      * @param $table as for {@link testing_db_record_builder::build_db_records()}
574      *      but does not need a unique first column.
575      */
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.");
582             }
583             $rec = array();
584             foreach ($columns as $i => $name) {
585                 $rec[$name] = $row[$i];
586             }
587             $this->records[] = $rec;
588         }
589         reset($this->records);
590     }
592     public function __destruct() {
593         $this->close();
594     }
596     public function current() {
597         return (object) current($this->records);
598     }
600     public function key() {
601         if (is_null(key($this->records))) {
602             return false;
603         }
604         $current = current($this->records);
605         return reset($current);
606     }
608     public function next() {
609         next($this->records);
610     }
612     public function valid() {
613         return !is_null(key($this->records));
614     }
616     public function close() {
617         $this->records = null;
618     }
622 /**
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.
626  *
627  * This class has failities for flipping $USER->id.
628  *
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.
632  *
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
637  */
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.');
653         }
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
673         );
674         $this->realcfg = $CFG;
675     }
677     /**
678      * Switch to using the test database for all queries until further notice.
679      */
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);
684         }
685         $DB = $this->testdb;
686     }
688     /**
689      * Revert to using the test database for all future queries.
690      */
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);
695         }
696         $DB = $this->realdb;
697     }
699     /**
700      * Switch to using the test $CFG for all queries until further notice.
701      */
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);
706         }
707         $CFG = $this->testcfg;
708     }
710     /**
711      * Revert to using the real $CFG for all future queries.
712      */
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);
717         }
718         $CFG = $this->realcfg;
719     }
721     /**
722      * Switch $USER->id to a test value.
723      *
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.
726      */
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;
733         }
734         $USER->id = $userid;
735     }
737     /**
738      * Revert $USER->id to the real value.
739      */
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;
747         }
748     }
750     /**
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.
753      */
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);
761         }
763         // Switch back to the real DB if necessary.
764         if ($DB !== $this->realdb) {
765             $this->revert_to_real_db();
766             $cleanmore = true;
767         }
769         // Switch back to the real CFG if necessary.
770         if (isset($CFG->testcfg)) {
771             $this->revert_to_real_cfg();
772             $cleanmore = true;
773         }
775         // revert_global_user_id if necessary.
776         if (!is_null($this->realuserid)) {
777             $this->revert_global_user_id();
778             $cleanmore = true;
779         }
781         if ($cleanmore) {
782             accesslib_clear_all_caches_for_unit_testing();
783             $course = 'reset';
784             get_fast_modinfo($course);
785         }
786     }
788     public function tearDown() {
789         $this->automatic_clean_up();
790         parent::tearDown();
791     }
793     public function __destruct() {
794         // Should not be necessary thanks to tearDown, but no harm in belt and braces.
795         $this->automatic_clean_up();
796     }
798     /**
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'.
805      */
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;
812         }
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));
816         }
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;
819     }
821     /**
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'.
827      */
828     protected function create_test_tables($tablenames, $installxmlfile) {
829         foreach ($tablenames as $tablename) {
830             $this->create_test_table($tablename, $installxmlfile);
831         }
832     }
834     /**
835      * Drop a test table.
836      * @param $tablename the name of the test table.
837      */
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;
842         }
843         $dbman = $this->testdb->get_manager();
844         $table = new xmldb_table($tablename);
845         $dbman->drop_table($table);
846         unset($this->tables[$tablename]);
847     }
849     /**
850      * Convenience method for calling drop_test_table repeatedly.
851      * @param array $tablenames an array of table names.
852      */
853     protected function drop_test_tables($tablenames) {
854         foreach ($tablenames as $tablename) {
855             $this->drop_test_table($tablename);
856         }
857     }
859     /**
860      * Load a table with some rows of data. A typical call would look like:
861      *
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      * ));
868      *
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.
873      */
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];
880             }
881             $obj->id = $this->testdb->insert_record($table, $obj);
882             $results[$rowid] = $obj;
883         }
884         return $results;
885     }
887     /**
888      * Clean up data loaded with load_test_data. The call corresponding to the
889      * example load above would be:
890      *
891      * $this->delete_test_data('config_plugins', $config);
892      *
893      * @param string $table the table name.
894      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
895      */
896     protected function delete_test_data($table, array $rows) {
897         $ids = array();
898         foreach ($rows as $row) {
899             $ids[] = $row->id;
900         }
901         $this->testdb->delete_records_list($table, 'id', $ids);
902     }
906 /**
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
911  */
912 class FakeDBUnitTestCase extends UnitTestCase {
913     public $tables = array();
914     public $pkfile;
915     public $cfg;
916     public $DB;
918     /**
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.
923      *
924      * throws moodle_exception if CSV file cannot be created
925      */
926     public function __construct($label = false) {
927         global $DB, $CFG;
929         if (empty($CFG->unittestprefix)) {
930             return;
931         }
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;
954                     }
955                     $tabledata .= "$table, $max_id\n";
956                 }
957             }
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);
962             }
963         }
964     }
966     /**
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
969      */
970     private function truncate_test_tables($tabledata) {
971         global $CFG, $DB;
973         if (empty($CFG->unittestprefix)) {
974             return;
975         }
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]));
982             }
983         }
984     }
986     /**
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
990      *
991      * throws moodle_exception if file doesn't exist
992      *
993      * @param string $filename
994      */
995     public function get_table_data($filename) {
996         global $CFG;
998         if (empty($CFG->unittestprefix)) {
999             return;
1000         }
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];
1008             }
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;
1015         }
1016     }
1018     /**
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)
1022      */
1023     public function setUp() {
1024         global $DB, $CFG;
1026         if (empty($CFG->unittestprefix)) {
1027             return;
1028         }
1030         parent::setUp();
1031         $this->DB =& $DB;
1032         ob_start();
1033     }
1035     /**
1036      * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
1037      */
1038     public function tearDown() {
1039         global $DB, $CFG;
1041         if (empty($CFG->unittestprefix)) {
1042             return;
1043         }
1045         if (empty($DB)) {
1046             $DB = $this->DB;
1047         }
1048         $DB->cleanup();
1049         parent::tearDown();
1051         // Output buffering
1052         if (ob_get_length() > 0) {
1053             ob_end_flush();
1054         }
1055     }
1057     /**
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.
1060      */
1061     public function __destruct() {
1062         global $CFG, $DB;
1064         if (empty($CFG->unittestprefix)) {
1065             return;
1066         }
1068         $CFG = $this->cfg;
1069         $this->tearDown();
1070         UnitTestDB::restore();
1071         fulldelete($this->pkfile);
1072     }
1074     /**
1075      * Load a table with some rows of data. A typical call would look like:
1076      *
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      * ));
1083      *
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.
1088      */
1089     public function load_test_data($table, array $cols, array $data) {
1090         global $CFG, $DB;
1092         if (empty($CFG->unittestprefix)) {
1093             return;
1094         }
1096         $results = array();
1097         foreach ($data as $rowid => $row) {
1098             $obj = new stdClass;
1099             foreach ($cols as $key => $colname) {
1100                 $obj->$colname = $row[$key];
1101             }
1102             $obj->id = $DB->insert_record($table, $obj);
1103             $results[$rowid] = $obj;
1104         }
1105         return $results;
1106     }
1108     /**
1109      * Clean up data loaded with load_test_data. The call corresponding to the
1110      * example load above would be:
1111      *
1112      * $this->delete_test_data('config_plugins', $config);
1113      *
1114      * @param string $table the table name.
1115      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
1116      */
1117     public function delete_test_data($table, array $rows) {
1118         global $CFG, $DB;
1120         if (empty($CFG->unittestprefix)) {
1121             return;
1122         }
1124         $ids = array();
1125         foreach ($rows as $row) {
1126             $ids[] = $row->id;
1127         }
1128         $DB->delete_records_list($table, 'id', $ids);
1129     }
1132 /**
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.
1138  *
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
1143  */
1144 class UnitTestDB {
1145     public static $DB;
1146     private static $real_db;
1148     public $table_data = array();
1150     /**
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.
1153      */
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');
1159         }
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);
1164         }
1166         $manager = UnitTestDB::$DB->get_manager();
1168         if (!$manager->table_exists('user')) {
1169             print_error('tablesnotsetup', 'simpletest');
1170         }
1172         $DB = new UnitTestDB();
1173     }
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);
1181         }
1182     }
1184     public function __get($variable) {
1185         return UnitTestDB::$DB->$variable;
1186     }
1188     public function __set($variable, $value) {
1189         UnitTestDB::$DB->$variable = $value;
1190     }
1192     public function __isset($variable) {
1193         return isset(UnitTestDB::$DB->$variable);
1194     }
1196     public function __unset($variable) {
1197         unset(UnitTestDB::$DB->$variable);
1198     }
1200     /**
1201      * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
1202      */
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;
1208     }
1210     /**
1211      * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
1212      * throw an exception and cancel update.
1213      *
1214      * throws moodle_exception If trying to update a record not inserted by unit tests.
1215      */
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);
1226         }
1227     }
1229     /**
1230      * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1231      * throw an exception and cancel delete.
1232      *
1233      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1234      */
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);
1245         }
1247         $proceed_with_delete = true;
1249         if (!is_array($ids_to_delete)) {
1250             $ids_to_delete = array($ids_to_delete);
1251         }
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;
1258             }
1259         }
1261         if ($proceed_with_delete) {
1262             return UnitTestDB::$DB->delete_records($table, $conditions);
1263         } else {
1264             throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1265         }
1266     }
1268     /**
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.
1271      *
1272      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1273      */
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);
1282         }
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;
1291             }
1292         }
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);
1298         }
1299     }
1301     /**
1302      * Removes from the test DB all the records that were inserted during unit tests,
1303      */
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));
1309             }
1310         }
1311     }
1313     /**
1314      * Restores the global $DB object.
1315      */
1316     public static function restore() {
1317         global $DB;
1318         $DB = UnitTestDB::$real_db;
1319     }
1321     public function get_field($table, $return, array $conditions) {
1322         if (!is_array($conditions)) {
1323             throw new coding_exception('$conditions is not an array.');
1324         }
1325         return UnitTestDB::$DB->get_field($table, $return, $conditions);
1326     }