MDL-29029 move all the simpletest bits into new tool unittest
[moodle.git] / admin / tool / unittest / simpletestlib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Utility functions to make unit testing easier.
19  *
20  * These functions, particularly the the database ones, are quick and
21  * dirty methods for getting things done in test cases. None of these
22  * methods should be used outside test code.
23  *
24  * Major Contirbutors
25  *     - T.J.Hunt@open.ac.uk
26  *
27  * @package    tool
28  * @subpackage unittest
29  * @copyright  &copy; 2006 The Open University
30  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31  */
33 defined('MOODLE_INTERNAL') || die();
35 /**
36  * Includes
37  */
38 require_once($CFG->libdir . '/simpletestlib/simpletest.php');
39 require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
40 require_once($CFG->libdir . '/simpletestlib/expectation.php');
41 require_once($CFG->libdir . '/simpletestlib/reporter.php');
42 require_once($CFG->libdir . '/simpletestlib/web_tester.php');
43 require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
45 /**
46  * Recursively visit all the files in the source tree. Calls the callback
47  * function with the pathname of each file found.
48  *
49  * @param $path the folder to start searching from.
50  * @param $callback the function to call with the name of each file found.
51  * @param $fileregexp a regexp used to filter the search (optional).
52  * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
53  *     only files that match the regexp will be included. (default false).
54  * @param array $ignorefolders will not go into any of these folders (optional).
55  */
56 function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
57     $files = scandir($path);
59     foreach ($files as $file) {
60         $filepath = $path .'/'. $file;
61         if (strpos($file, '.') === 0) {
62             /// Don't check hidden files.
63             continue;
64         } else if (is_dir($filepath)) {
65             if (!in_array($filepath, $ignorefolders)) {
66                 recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
67             }
68         } else if ($exclude xor preg_match($fileregexp, $filepath)) {
69             call_user_func($callback, $filepath);
70         }
71     }
72 }
74 /**
75  * An expectation for comparing strings ignoring whitespace.
76  *
77  * @package moodlecore
78  * @subpackage simpletestex
79  * @copyright &copy; 2006 The Open University
80  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
81  */
82 class IgnoreWhitespaceExpectation extends SimpleExpectation {
83     var $expect;
85     function IgnoreWhitespaceExpectation($content, $message = '%s') {
86         $this->SimpleExpectation($message);
87         $this->expect=$this->normalise($content);
88     }
90     function test($ip) {
91         return $this->normalise($ip)==$this->expect;
92     }
94     function normalise($text) {
95         return preg_replace('/\s+/m',' ',trim($text));
96     }
98     function testMessage($ip) {
99         return "Input string [$ip] doesn't match the required value.";
100     }
103 /**
104  * An Expectation that two arrays contain the same list of values.
105  *
106  * @package moodlecore
107  * @subpackage simpletestex
108  * @copyright &copy; 2006 The Open University
109  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
110  */
111 class ArraysHaveSameValuesExpectation extends SimpleExpectation {
112     var $expect;
114     function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
115         $this->SimpleExpectation($message);
116         if (!is_array($expected)) {
117             trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
118                     'with an expected value that is not an array.');
119         }
120         $this->expect = $this->normalise($expected);
121     }
123     function test($actual) {
124         return $this->normalise($actual) == $this->expect;
125     }
127     function normalise($array) {
128         sort($array);
129         return $array;
130     }
132     function testMessage($actual) {
133         return 'Array [' . implode(', ', $actual) .
134                 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
135     }
139 /**
140  * An Expectation that compares to objects, and ensures that for every field in the
141  * expected object, there is a key of the same name in the actual object, with
142  * the same value. (The actual object may have other fields to, but we ignore them.)
143  *
144  * @package moodlecore
145  * @subpackage simpletestex
146  * @copyright &copy; 2006 The Open University
147  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
148  */
149 class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
150     var $expect;
152     function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
153         $this->SimpleExpectation($message);
154         if (!is_object($expected)) {
155             trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
156                     'with an expected value that is not an object.');
157         }
158         $this->expect = $expected;
159     }
161     function test($actual) {
162         foreach ($this->expect as $key => $value) {
163             if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
164                 // OK
165             } else if (is_null($value) && is_null($actual->$key)) {
166                 // OK
167             } else {
168                 return false;
169             }
170         }
171         return true;
172     }
174     function testMessage($actual) {
175         $mismatches = array();
176         foreach ($this->expect as $key => $value) {
177             if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
178                 // OK
179             } else if (is_null($value) && is_null($actual->$key)) {
180                 // OK
181             } else if (!isset($actual->$key)) {
182                 $mismatches[] = $key . ' (expected [' . $value . '] but was missing.';
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     function testMessage($html) {
210         $parsererrors = $this->load_xml($html);
211         if (is_array($parsererrors)) {
212             foreach ($parsererrors as $key => $message) {
213                 $parsererrors[$key] = $message->message;
214             }
215             return 'Could not parse XML [' . $html . '] errors were [' .
216                     implode('], [', $parsererrors) . ']';
217         }
218         return $this->customMessage($html);
219     }
221 /**
222  * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
223  *
224  * @copyright 2009 Tim Hunt
225  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
226  */
227 class ContainsTagWithAttribute extends XMLStructureExpectation {
228     protected $tag;
229     protected $attribute;
230     protected $value;
232     function __construct($tag, $attribute, $value, $message = '%s') {
233         parent::__construct($message);
234         $this->tag = $tag;
235         $this->attribute = $attribute;
236         $this->value = $value;
237     }
239     function test($html) {
240         $parser = $this->load_xml($html);
241         if (is_array($parser)) {
242             return false;
243         }
244         $list = $parser->getElementsByTagName($this->tag);
246         foreach ($list as $node) {
247             if ($node->attributes->getNamedItem($this->attribute)->nodeValue === (string) $this->value) {
248                 return true;
249             }
250         }
251         return false;
252     }
254     function customMessage($html) {
255         return 'Content [' . $html . '] does not contain the tag [' .
256                 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
257     }
260 /**
261  * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
262  * All attributes must be present and their values must match the expected values.
263  * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
264  *
265  * @copyright 2009 Nicolas Connault
266  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
267  */
268 class ContainsTagWithAttributes extends XMLStructureExpectation {
269     /**
270      * @var string $tag The name of the Tag to search
271      */
272     protected $tag;
273     /**
274      * @var array $expectedvalues An associative array of parameters, all of which must be matched
275      */
276     protected $expectedvalues = array();
277     /**
278      * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
279      */
280     protected $forbiddenvalues = array();
281     /**
282      * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
283      */
284     protected $failurereason = 'nomatch';
286     function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
287         parent::__construct($message);
288         $this->tag = $tag;
289         $this->expectedvalues = $expectedvalues;
290         $this->forbiddenvalues = $forbiddenvalues;
291     }
293     function test($html) {
294         $parser = $this->load_xml($html);
295         if (is_array($parser)) {
296             return false;
297         }
299         $list = $parser->getElementsByTagName($this->tag);
300         $foundamatch = false;
302         // Iterating through inputs
303         foreach ($list as $node) {
304             if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
305                 continue;
306             }
308             // For the current expected attribute under consideration, check that values match
309             $allattributesmatch = true;
311             foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
312                 if ($node->getAttribute($expectedattribute) === '' && $expectedvalue !== '') {
313                     $this->failurereason = 'nomatch';
314                     continue 2; // Skip this tag, it doesn't have all the expected attributes
315                 }
316                 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
317                     $allattributesmatch = false;
318                     $this->failurereason = 'nomatch';
319                 }
320             }
322             if ($allattributesmatch) {
323                 $foundamatch = true;
325                 // Now make sure this node doesn't have any of the forbidden attributes either
326                 $nodeattrlist = $node->attributes;
328                 foreach ($nodeattrlist as $domattrname => $domattr) {
329                     if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) === (string) $this->forbiddenvalues[$domattrname]) {
330                         $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
331                         $foundamatch = false;
332                     }
333                 }
334             }
335         }
337         return $foundamatch;
338     }
340     function customMessage($html) {
341         $output = 'Content [' . $html . '] ';
343         if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
344             $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
345         } else if ($this->failurereason == 'nomatch') {
346             $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
347             foreach ($this->expectedvalues as $var => $val) {
348                 $output .= "$var=\"$val\" ";
349             }
350             $output = rtrim($output);
351             $output .= '].';
352         }
354         return $output;
355     }
358 /**
359  * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
360  * All attributes must be present and their values must match the expected values.
361  * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
362  *
363  * @copyright 2010 The Open University
364  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
365  */
366 class ContainsSelectExpectation extends XMLStructureExpectation {
367     /**
368      * @var string $tag The name of the Tag to search
369      */
370     protected $name;
371     /**
372      * @var array $expectedvalues An associative array of parameters, all of which must be matched
373      */
374     protected $choices;
375     /**
376      * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
377      */
378     protected $selected;
379     /**
380      * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
381      */
382     protected $enabled;
384     function __construct($name, $choices, $selected = null, $enabled = null, $message = '%s') {
385         parent::__construct($message);
386         $this->name = $name;
387         $this->choices = $choices;
388         $this->selected = $selected;
389         $this->enabled = $enabled;
390     }
392     function test($html) {
393         $parser = $this->load_xml($html);
394         if (is_array($parser)) {
395             return false;
396         }
398         $list = $parser->getElementsByTagName('select');
400         // Iterating through inputs
401         foreach ($list as $node) {
402             if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
403                 continue;
404             }
406             if ($node->getAttribute('name') != $this->name) {
407                 continue;
408             }
410             if ($this->enabled === true && $node->getAttribute('disabled')) {
411                 continue;
412             } else if ($this->enabled === false && $node->getAttribute('disabled') != 'disabled') {
413                 continue;
414             }
416             $options = $node->getElementsByTagName('option');
417             reset($this->choices);
418             foreach ($options as $option) {
419                 if ($option->getAttribute('value') != key($this->choices)) {
420                     continue 2;
421                 }
422                 if ($option->firstChild->wholeText != current($this->choices)) {
423                     continue 2;
424                 }
425                 if ($option->getAttribute('value') === $this->selected &&
426                         !$option->hasAttribute('selected')) {
427                     continue 2;
428                 }
429                 next($this->choices);
430             }
431             if (current($this->choices) !== false) {
432                 // The HTML did not contain all the choices.
433                 return false;
434             }
435             return true;
436         }
437         return false;
438     }
440     function customMessage($html) {
441         if ($this->enabled === true) {
442             $state = 'an enabled';
443         } else if ($this->enabled === false) {
444             $state = 'a disabled';
445         } else {
446             $state = 'a';
447         }
448         $output = 'Content [' . $html . '] does not contain ' . $state .
449                 ' <select> with name ' . $this->name . ' and choices ' .
450                 implode(', ', $this->choices);
451         if ($this->selected) {
452             $output .= ' with ' . $this->selected . ' selected).';
453         }
455         return $output;
456     }
459 /**
460  * The opposite of {@link ContainsTagWithAttributes}. The test passes only if
461  * the HTML does not contain a tag with the given attributes.
462  *
463  * @copyright 2010 The Open University
464  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
465  */
466 class DoesNotContainTagWithAttributes extends ContainsTagWithAttributes {
467     function __construct($tag, $expectedvalues, $message = '%s') {
468         parent::__construct($tag, $expectedvalues, array(), $message);
469     }
470     function test($html) {
471         return !parent::test($html);
472     }
473     function customMessage($html) {
474         $output = 'Content [' . $html . '] ';
476         $output .= 'contains the tag [' . $this->tag . '] with attributes [';
477         foreach ($this->expectedvalues as $var => $val) {
478             $output .= "$var=\"$val\" ";
479         }
480         $output = rtrim($output);
481         $output .= '].';
483         return $output;
484     }
487 /**
488  * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
489  *
490  * @copyright 2009 Tim Hunt
491  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
492  */
493 class ContainsTagWithContents extends XMLStructureExpectation {
494     protected $tag;
495     protected $content;
497     function __construct($tag, $content, $message = '%s') {
498         parent::__construct($message);
499         $this->tag = $tag;
500         $this->content = $content;
501     }
503     function test($html) {
504         $parser = $this->load_xml($html);
505         $list = $parser->getElementsByTagName($this->tag);
507         foreach ($list as $node) {
508             if ($node->textContent == $this->content) {
509                 return true;
510             }
511         }
513         return false;
514     }
516     function testMessage($html) {
517         return 'Content [' . $html . '] does not contain the tag [' .
518                 $this->tag . '] with contents [' . $this->content . '].';
519     }
522 /**
523  * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
524  *
525  * @copyright 2009 Nicolas Connault
526  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
527  */
528 class ContainsEmptyTag extends XMLStructureExpectation {
529     protected $tag;
531     function __construct($tag, $message = '%s') {
532         parent::__construct($message);
533         $this->tag = $tag;
534     }
536     function test($html) {
537         $parser = $this->load_xml($html);
538         $list = $parser->getElementsByTagName($this->tag);
540         foreach ($list as $node) {
541             if (!$node->hasAttributes() && !$node->hasChildNodes()) {
542                 return true;
543             }
544         }
546         return false;
547     }
549     function testMessage($html) {
550         return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
551     }
555 /**
556  * Simple class that implements the {@link moodle_recordset} API based on an
557  * array of test data.
558  *
559  *  See the {@link question_attempt_step_db_test} class in
560  *  question/engine/simpletest/testquestionattemptstep.php for an example of how
561  *  this is used.
562  *
563  * @copyright  2011 The Open University
564  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
565  */
566 class test_recordset extends moodle_recordset {
567     protected $records;
569     /**
570      * Constructor
571      * @param $table as for {@link testing_db_record_builder::build_db_records()}
572      *      but does not need a unique first column.
573      */
574     public function __construct(array $table) {
575         $columns = array_shift($table);
576         $this->records = array();
577         foreach ($table as $row) {
578             if (count($row) != count($columns)) {
579                 throw new coding_exception("Row contains the wrong number of fields.");
580             }
581             $rec = array();
582             foreach ($columns as $i => $name) {
583                 $rec[$name] = $row[$i];
584             }
585             $this->records[] = $rec;
586         }
587         reset($this->records);
588     }
590     public function __destruct() {
591         $this->close();
592     }
594     public function current() {
595         return (object) current($this->records);
596     }
598     public function key() {
599         if (is_null(key($this->records))) {
600             return false;
601         }
602         $current = current($this->records);
603         return reset($current);
604     }
606     public function next() {
607         next($this->records);
608     }
610     public function valid() {
611         return !is_null(key($this->records));
612     }
614     public function close() {
615         $this->records = null;
616     }
620 /**
621  * This class lets you write unit tests that access a separate set of test
622  * tables with a different prefix. Only those tables you explicitly ask to
623  * be created will be.
624  *
625  * This class has failities for flipping $USER->id.
626  *
627  * The tear-down method for this class should automatically revert any changes
628  * you make during test set-up using the metods defined here. That is, it will
629  * drop tables for you automatically and revert to the real $DB and $USER->id.
630  *
631  * @package moodlecore
632  * @subpackage simpletestex
633  * @copyright &copy; 2006 The Open University
634  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
635  */
636 class UnitTestCaseUsingDatabase extends UnitTestCase {
637     private $realdb;
638     protected $testdb;
639     private $realuserid = null;
640     private $tables = array();
642     private $realcfg;
643     protected $testcfg;
645     public function __construct($label = false) {
646         global $DB, $CFG;
648         // Complain if we get this far and $CFG->unittestprefix is not set.
649         if (empty($CFG->unittestprefix)) {
650             throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
651         }
653         // Only do this after the above text.
654         parent::UnitTestCase($label);
656         // Create the test DB instance.
657         $this->realdb = $DB;
658         $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
659         $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
661         // Set up test config
662         $this->testcfg = (object)array(
663                 'testcfg' => true, // Marker that this is a test config
664                 'libdir' => $CFG->libdir, // Must use real one so require_once works
665                 'dirroot' => $CFG->dirroot, // Must use real one
666                 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
667                 'ostype' => $CFG->ostype, // Real one
668                 'wwwroot' => 'http://www.example.org', // Use fixed url
669                 'siteadmins' => '0', // No admins
670                 'siteguest' => '0' // No guest
671         );
672         $this->realcfg = $CFG;
673     }
675     /**
676      * Switch to using the test database for all queries until further notice.
677      */
678     protected function switch_to_test_db() {
679         global $DB;
680         if ($DB === $this->testdb) {
681             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);
682         }
683         $DB = $this->testdb;
684     }
686     /**
687      * Revert to using the test database for all future queries.
688      */
689     protected function revert_to_real_db() {
690         global $DB;
691         if ($DB !== $this->testdb) {
692             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);
693         }
694         $DB = $this->realdb;
695     }
697     /**
698      * Switch to using the test $CFG for all queries until further notice.
699      */
700     protected function switch_to_test_cfg() {
701         global $CFG;
702         if (isset($CFG->testcfg)) {
703             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);
704         }
705         $CFG = $this->testcfg;
706     }
708     /**
709      * Revert to using the real $CFG for all future queries.
710      */
711     protected function revert_to_real_cfg() {
712         global $CFG;
713         if (!isset($CFG->testcfg)) {
714             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);
715         }
716         $CFG = $this->realcfg;
717     }
719     /**
720      * Switch $USER->id to a test value.
721      *
722      * It might be worth making this method do more robuse $USER switching in future,
723      * however, this is sufficient for my needs at present.
724      */
725     protected function switch_global_user_id($userid) {
726         global $USER;
727         if (!is_null($this->realuserid)) {
728             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);
729         } else {
730             $this->realuserid = $USER->id;
731         }
732         $USER->id = $userid;
733     }
735     /**
736      * Revert $USER->id to the real value.
737      */
738     protected function revert_global_user_id() {
739         global $USER;
740         if (is_null($this->realuserid)) {
741             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);
742         } else {
743             $USER->id = $this->realuserid;
744             $this->realuserid = null;
745         }
746     }
748     /**
749      * Check that the user has not forgotten to clean anything up, and if they
750      * have, display a rude message and clean it up for them.
751      */
752     private function automatic_clean_up() {
753         global $DB, $CFG;
754         $cleanmore = false;
756         // Drop any test tables that were created.
757         foreach ($this->tables as $tablename => $notused) {
758             $this->drop_test_table($tablename);
759         }
761         // Switch back to the real DB if necessary.
762         if ($DB !== $this->realdb) {
763             $this->revert_to_real_db();
764             $cleanmore = true;
765         }
767         // Switch back to the real CFG if necessary.
768         if (isset($CFG->testcfg)) {
769             $this->revert_to_real_cfg();
770             $cleanmore = true;
771         }
773         // revert_global_user_id if necessary.
774         if (!is_null($this->realuserid)) {
775             $this->revert_global_user_id();
776             $cleanmore = true;
777         }
779         if ($cleanmore) {
780             accesslib_clear_all_caches_for_unit_testing();
781             $course = 'reset';
782             get_fast_modinfo($course);
783         }
784     }
786     public function tearDown() {
787         $this->automatic_clean_up();
788         parent::tearDown();
789     }
791     public function __destruct() {
792         // Should not be necessary thanks to tearDown, but no harm in belt and braces.
793         $this->automatic_clean_up();
794     }
796     /**
797      * Create a test table just like a real one, getting getting the definition from
798      * the specified install.xml file.
799      * @param string $tablename the name of the test table.
800      * @param string $installxmlfile the install.xml file in which this table is defined.
801      *      $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
802      *      so you need only specify, for example, 'mod/quiz'.
803      */
804     protected function create_test_table($tablename, $installxmlfile) {
805         global $CFG;
806         $dbman = $this->testdb->get_manager();
807         if (isset($this->tables[$tablename])) {
808             debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
809             return;
810         }
811         if ($dbman->table_exists($tablename)) {
812             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);
813             $dbman->drop_table(new xmldb_table($tablename));
814         }
815         $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
816         $this->tables[$tablename] = 1;
817     }
819     /**
820      * Convenience method for calling create_test_table repeatedly.
821      * @param array $tablenames an array of table names.
822      * @param string $installxmlfile the install.xml file in which this table is defined.
823      *      $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
824      *      so you need only specify, for example, 'mod/quiz'.
825      */
826     protected function create_test_tables($tablenames, $installxmlfile) {
827         foreach ($tablenames as $tablename) {
828             $this->create_test_table($tablename, $installxmlfile);
829         }
830     }
832     /**
833      * Drop a test table.
834      * @param $tablename the name of the test table.
835      */
836     protected function drop_test_table($tablename) {
837         if (!isset($this->tables[$tablename])) {
838             debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
839             return;
840         }
841         $dbman = $this->testdb->get_manager();
842         $table = new xmldb_table($tablename);
843         $dbman->drop_table($table);
844         unset($this->tables[$tablename]);
845     }
847     /**
848      * Convenience method for calling drop_test_table repeatedly.
849      * @param array $tablenames an array of table names.
850      */
851     protected function drop_test_tables($tablenames) {
852         foreach ($tablenames as $tablename) {
853             $this->drop_test_table($tablename);
854         }
855     }
857     /**
858      * Load a table with some rows of data. A typical call would look like:
859      *
860      * $config = $this->load_test_data('config_plugins',
861      *         array('plugin', 'name', 'value'), array(
862      *         array('frog', 'numlegs', 2),
863      *         array('frog', 'sound', 'croak'),
864      *         array('frog', 'action', 'jump'),
865      * ));
866      *
867      * @param string $table the table name.
868      * @param array $cols the columns to fill.
869      * @param array $data the data to load.
870      * @return array $objects corresponding to $data.
871      */
872     protected function load_test_data($table, array $cols, array $data) {
873         $results = array();
874         foreach ($data as $rowid => $row) {
875             $obj = new stdClass;
876             foreach ($cols as $key => $colname) {
877                 $obj->$colname = $row[$key];
878             }
879             $obj->id = $this->testdb->insert_record($table, $obj);
880             $results[$rowid] = $obj;
881         }
882         return $results;
883     }
885     /**
886      * Clean up data loaded with load_test_data. The call corresponding to the
887      * example load above would be:
888      *
889      * $this->delete_test_data('config_plugins', $config);
890      *
891      * @param string $table the table name.
892      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
893      */
894     protected function delete_test_data($table, array $rows) {
895         $ids = array();
896         foreach ($rows as $row) {
897             $ids[] = $row->id;
898         }
899         $this->testdb->delete_records_list($table, 'id', $ids);
900     }
904 /**
905  * @package moodlecore
906  * @subpackage simpletestex
907  * @copyright &copy; 2006 The Open University
908  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
909  */
910 class FakeDBUnitTestCase extends UnitTestCase {
911     public $tables = array();
912     public $pkfile;
913     public $cfg;
914     public $DB;
916     /**
917      * In the constructor, record the max(id) of each test table into a csv file.
918      * If this file already exists, it means that a previous run of unit tests
919      * did not complete, and has left data undeleted in the DB. This data is then
920      * deleted and the file is retained. Otherwise it is created.
921      *
922      * throws moodle_exception if CSV file cannot be created
923      */
924     public function __construct($label = false) {
925         global $DB, $CFG;
927         if (empty($CFG->unittestprefix)) {
928             return;
929         }
931         parent::UnitTestCase($label);
932         // MDL-16483 Get PKs and save data to text file
934         $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
935         $this->cfg = $CFG;
937         UnitTestDB::instantiate();
939         $tables = $DB->get_tables();
941         // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
942         if (file_exists($this->pkfile)) {
943             $this->truncate_test_tables($this->get_table_data($this->pkfile));
945         } else { // Create the file
946             $tabledata = '';
948             foreach ($tables as $table) {
949                 if ($table != 'sessions') {
950                     if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
951                         $max_id = 0;
952                     }
953                     $tabledata .= "$table, $max_id\n";
954                 }
955             }
956             if (!file_put_contents($this->pkfile, $tabledata)) {
957                 $a = new stdClass();
958                 $a->filename = $this->pkfile;
959                 throw new moodle_exception('testtablescsvfileunwritable', 'tool_unittest', '', $a);
960             }
961         }
962     }
964     /**
965      * 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.
966      * @param array $tabledata
967      */
968     private function truncate_test_tables($tabledata) {
969         global $CFG, $DB;
971         if (empty($CFG->unittestprefix)) {
972             return;
973         }
975         $tables = $DB->get_tables();
977         foreach ($tables as $table) {
978             if ($table != 'sessions' && isset($tabledata[$table])) {
979                 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
980             }
981         }
982     }
984     /**
985      * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
986      * 1. Table name
987      * 2. Max id
988      *
989      * throws moodle_exception if file doesn't exist
990      *
991      * @param string $filename
992      */
993     public function get_table_data($filename) {
994         global $CFG;
996         if (empty($CFG->unittestprefix)) {
997             return;
998         }
1000         if (file_exists($this->pkfile)) {
1001             $handle = fopen($this->pkfile, 'r');
1002             $tabledata = array();
1004             while (($data = fgetcsv($handle, 1000, ",")) !== false) {
1005                 $tabledata[$data[0]] = $data[1];
1006             }
1007             return $tabledata;
1008         } else {
1009             $a = new stdClass();
1010             $a->filename = $this->pkfile;
1011             throw new moodle_exception('testtablescsvfilemissing', 'tool_unittest', '', $a);
1012             return false;
1013         }
1014     }
1016     /**
1017      * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
1018      * Also detects if this config setting is properly set, and if the user table exists.
1019      * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
1020      */
1021     public function setUp() {
1022         global $DB, $CFG;
1024         if (empty($CFG->unittestprefix)) {
1025             return;
1026         }
1028         parent::setUp();
1029         $this->DB =& $DB;
1030         ob_start();
1031     }
1033     /**
1034      * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
1035      */
1036     public function tearDown() {
1037         global $DB, $CFG;
1039         if (empty($CFG->unittestprefix)) {
1040             return;
1041         }
1043         if (empty($DB)) {
1044             $DB = $this->DB;
1045         }
1046         $DB->cleanup();
1047         parent::tearDown();
1049         // Output buffering
1050         if (ob_get_length() > 0) {
1051             ob_end_flush();
1052         }
1053     }
1055     /**
1056      * 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
1057      * It should also detect if data is missing from the original tables.
1058      */
1059     public function __destruct() {
1060         global $CFG, $DB;
1062         if (empty($CFG->unittestprefix)) {
1063             return;
1064         }
1066         $CFG = $this->cfg;
1067         $this->tearDown();
1068         UnitTestDB::restore();
1069         fulldelete($this->pkfile);
1070     }
1072     /**
1073      * Load a table with some rows of data. A typical call would look like:
1074      *
1075      * $config = $this->load_test_data('config_plugins',
1076      *         array('plugin', 'name', 'value'), array(
1077      *         array('frog', 'numlegs', 2),
1078      *         array('frog', 'sound', 'croak'),
1079      *         array('frog', 'action', 'jump'),
1080      * ));
1081      *
1082      * @param string $table the table name.
1083      * @param array $cols the columns to fill.
1084      * @param array $data the data to load.
1085      * @return array $objects corresponding to $data.
1086      */
1087     public function load_test_data($table, array $cols, array $data) {
1088         global $CFG, $DB;
1090         if (empty($CFG->unittestprefix)) {
1091             return;
1092         }
1094         $results = array();
1095         foreach ($data as $rowid => $row) {
1096             $obj = new stdClass;
1097             foreach ($cols as $key => $colname) {
1098                 $obj->$colname = $row[$key];
1099             }
1100             $obj->id = $DB->insert_record($table, $obj);
1101             $results[$rowid] = $obj;
1102         }
1103         return $results;
1104     }
1106     /**
1107      * Clean up data loaded with load_test_data. The call corresponding to the
1108      * example load above would be:
1109      *
1110      * $this->delete_test_data('config_plugins', $config);
1111      *
1112      * @param string $table the table name.
1113      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
1114      */
1115     public function delete_test_data($table, array $rows) {
1116         global $CFG, $DB;
1118         if (empty($CFG->unittestprefix)) {
1119             return;
1120         }
1122         $ids = array();
1123         foreach ($rows as $row) {
1124             $ids[] = $row->id;
1125         }
1126         $DB->delete_records_list($table, 'id', $ids);
1127     }
1130 /**
1131  * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
1132  * static instantiate() method, and restores the original global $DB through restore().
1133  * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
1134  * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
1135  * without subclassing it.
1136  *
1137  * @package moodlecore
1138  * @subpackage simpletestex
1139  * @copyright &copy; 2006 The Open University
1140  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1141  */
1142 class UnitTestDB {
1143     public static $DB;
1144     private static $real_db;
1146     public $table_data = array();
1148     /**
1149      * Call this statically to connect to the DB using the unittest prefix, instantiate
1150      * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
1151      */
1152     public static function instantiate() {
1153         global $CFG, $DB;
1154         UnitTestDB::$real_db = clone($DB);
1155         if (empty($CFG->unittestprefix)) {
1156             print_error("prefixnotset", 'tool_unittest');
1157         }
1159         if (empty(UnitTestDB::$DB)) {
1160             UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
1161             UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
1162         }
1164         $manager = UnitTestDB::$DB->get_manager();
1166         if (!$manager->table_exists('user')) {
1167             print_error('tablesnotsetup', 'tool_unittest');
1168         }
1170         $DB = new UnitTestDB();
1171     }
1173     public function __call($method, $args) {
1174         // Set args to null if they don't exist (up to 10 args should do)
1175         if (!method_exists($this, $method)) {
1176             return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
1177         } else {
1178             call_user_func_array(array($this, $method), $args);
1179         }
1180     }
1182     public function __get($variable) {
1183         return UnitTestDB::$DB->$variable;
1184     }
1186     public function __set($variable, $value) {
1187         UnitTestDB::$DB->$variable = $value;
1188     }
1190     public function __isset($variable) {
1191         return isset(UnitTestDB::$DB->$variable);
1192     }
1194     public function __unset($variable) {
1195         unset(UnitTestDB::$DB->$variable);
1196     }
1198     /**
1199      * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
1200      */
1201     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1202         global $DB;
1203         $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
1204         $this->table_data[$table][] = $id;
1205         return $id;
1206     }
1208     /**
1209      * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
1210      * throw an exception and cancel update.
1211      *
1212      * throws moodle_exception If trying to update a record not inserted by unit tests.
1213      */
1214     public function update_record($table, $dataobject, $bulk=false) {
1215         global $DB;
1216         if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
1217             // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1218             $a = new stdClass();
1219             $a->id = $dataobject->id;
1220             $a->table = $table;
1221             throw new moodle_exception('updatingnoninsertedrecord', 'tool_unittest', '', $a);
1222         } else {
1223             return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1224         }
1225     }
1227     /**
1228      * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1229      * throw an exception and cancel delete.
1230      *
1231      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1232      */
1233     public function delete_records($table, array $conditions=array()) {
1234         global $DB;
1235         $tables_to_ignore = array('context_temp');
1237         $a = new stdClass();
1238         $a->table = $table;
1240         // Get ids matching conditions
1241         if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
1242             return UnitTestDB::$DB->delete_records($table, $conditions);
1243         }
1245         $proceed_with_delete = true;
1247         if (!is_array($ids_to_delete)) {
1248             $ids_to_delete = array($ids_to_delete);
1249         }
1251         foreach ($ids_to_delete as $id) {
1252             if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
1253                 $proceed_with_delete = false;
1254                 $a->id = $id;
1255                 break;
1256             }
1257         }
1259         if ($proceed_with_delete) {
1260             return UnitTestDB::$DB->delete_records($table, $conditions);
1261         } else {
1262             throw new moodle_exception('deletingnoninsertedrecord', 'tool_unittest', '', $a);
1263         }
1264     }
1266     /**
1267      * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1268      * throw an exception and cancel delete.
1269      *
1270      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1271      */
1272     public function delete_records_select($table, $select, array $params=null) {
1273         global $DB;
1274         $a = new stdClass();
1275         $a->table = $table;
1277         // Get ids matching conditions
1278         if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1279             return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1280         }
1282         $proceed_with_delete = true;
1284         foreach ($ids_to_delete as $id) {
1285             if (!in_array($id, $this->table_data[$table])) {
1286                 $proceed_with_delete = false;
1287                 $a->id = $id;
1288                 break;
1289             }
1290         }
1292         if ($proceed_with_delete) {
1293             return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1294         } else {
1295             throw new moodle_exception('deletingnoninsertedrecord', 'tool_unittest', '', $a);
1296         }
1297     }
1299     /**
1300      * Removes from the test DB all the records that were inserted during unit tests,
1301      */
1302     public function cleanup() {
1303         global $DB;
1304         foreach ($this->table_data as $table => $ids) {
1305             foreach ($ids as $id) {
1306                 $DB->delete_records($table, array('id' => $id));
1307             }
1308         }
1309     }
1311     /**
1312      * Restores the global $DB object.
1313      */
1314     public static function restore() {
1315         global $DB;
1316         $DB = UnitTestDB::$real_db;
1317     }
1319     public function get_field($table, $return, array $conditions) {
1320         if (!is_array($conditions)) {
1321             throw new coding_exception('$conditions is not an array.');
1322         }
1323         return UnitTestDB::$DB->get_field($table, $return, $conditions);
1324     }