MDL-25715 simpletestlib new expectations for testing renderer output.
[moodle.git] / lib / simpletestlib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Utility functions to make unit testing easier.
20  *
21  * These functions, particularly the the database ones, are quick and
22  * dirty methods for getting things done in test cases. None of these
23  * methods should be used outside test code.
24  *
25  * Major Contirbutors
26  *     - T.J.Hunt@open.ac.uk
27  *
28  * @package    core
29  * @subpackage simpletestex
30  * @copyright  &copy; 2006 The Open University
31  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32  */
34 defined('MOODLE_INTERNAL') || die();
36 /**
37  * Includes
38  */
39 require_once(dirname(__FILE__) . '/../config.php');
40 require_once($CFG->libdir . '/simpletestlib/simpletest.php');
41 require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
42 require_once($CFG->libdir . '/simpletestlib/expectation.php');
43 require_once($CFG->libdir . '/simpletestlib/reporter.php');
44 require_once($CFG->libdir . '/simpletestlib/web_tester.php');
45 require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
47 /**
48  * Recursively visit all the files in the source tree. Calls the callback
49  * function with the pathname of each file found.
50  *
51  * @param $path the folder to start searching from.
52  * @param $callback the function to call with the name of each file found.
53  * @param $fileregexp a regexp used to filter the search (optional).
54  * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
55  *     only files that match the regexp will be included. (default false).
56  * @param array $ignorefolders will not go into any of these folders (optional).
57  */
58 function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
59     $files = scandir($path);
61     foreach ($files as $file) {
62         $filepath = $path .'/'. $file;
63         if (strpos($file, '.') === 0) {
64             /// Don't check hidden files.
65             continue;
66         } else if (is_dir($filepath)) {
67             if (!in_array($filepath, $ignorefolders)) {
68                 recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
69             }
70         } else if ($exclude xor preg_match($fileregexp, $filepath)) {
71             call_user_func($callback, $filepath);
72         }
73     }
74 }
76 /**
77  * An expectation for comparing strings ignoring whitespace.
78  *
79  * @package moodlecore
80  * @subpackage simpletestex
81  * @copyright &copy; 2006 The Open University
82  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
83  */
84 class IgnoreWhitespaceExpectation extends SimpleExpectation {
85     var $expect;
87     function IgnoreWhitespaceExpectation($content, $message = '%s') {
88         $this->SimpleExpectation($message);
89         $this->expect=$this->normalise($content);
90     }
92     function test($ip) {
93         return $this->normalise($ip)==$this->expect;
94     }
96     function normalise($text) {
97         return preg_replace('/\s+/m',' ',trim($text));
98     }
100     function testMessage($ip) {
101         return "Input string [$ip] doesn't match the required value.";
102     }
105 /**
106  * An Expectation that two arrays contain the same list of values.
107  *
108  * @package moodlecore
109  * @subpackage simpletestex
110  * @copyright &copy; 2006 The Open University
111  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
112  */
113 class ArraysHaveSameValuesExpectation extends SimpleExpectation {
114     var $expect;
116     function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
117         $this->SimpleExpectation($message);
118         if (!is_array($expected)) {
119             trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
120                     'with an expected value that is not an array.');
121         }
122         $this->expect = $this->normalise($expected);
123     }
125     function test($actual) {
126         return $this->normalise($actual) == $this->expect;
127     }
129     function normalise($array) {
130         sort($array);
131         return $array;
132     }
134     function testMessage($actual) {
135         return 'Array [' . implode(', ', $actual) .
136                 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
137     }
141 /**
142  * An Expectation that compares to objects, and ensures that for every field in the
143  * expected object, there is a key of the same name in the actual object, with
144  * the same value. (The actual object may have other fields to, but we ignore them.)
145  *
146  * @package moodlecore
147  * @subpackage simpletestex
148  * @copyright &copy; 2006 The Open University
149  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
150  */
151 class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
152     var $expect;
154     function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
155         $this->SimpleExpectation($message);
156         if (!is_object($expected)) {
157             trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
158                     'with an expected value that is not an object.');
159         }
160         $this->expect = $expected;
161     }
163     function test($actual) {
164         foreach ($this->expect as $key => $value) {
165             if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
166                 // OK
167             } else if (is_null($value) && is_null($actual->$key)) {
168                 // OK
169             } else {
170                 return false;
171             }
172         }
173         return true;
174     }
176     function testMessage($actual) {
177         $mismatches = array();
178         foreach ($this->expect as $key => $value) {
179             if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
180                 // OK
181             } else if (is_null($value) && is_null($actual->$key)) {
182                 // OK
183             } else {
184                 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
185             }
186         }
187         return 'Actual object does not have all the same fields with the same values as the expected object (' .
188                 implode(', ', $mismatches) . ').';
189     }
192 abstract class XMLStructureExpectation extends SimpleExpectation {
193     /**
194      * Parse a string as XML and return a DOMDocument;
195      * @param $html
196      * @return unknown_type
197      */
198     protected function load_xml($html) {
199         $prevsetting = libxml_use_internal_errors(true);
200         $parser = new DOMDocument();
201         if (!$parser->loadXML('<html>' . $html . '</html>')) {
202             $parser = new DOMDocument();
203         }
204         libxml_clear_errors();
205         libxml_use_internal_errors($prevsetting);
206         return $parser;
207     }
209     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  * This class lets you write unit tests that access a separate set of test
557  * tables with a different prefix. Only those tables you explicitly ask to
558  * be created will be.
559  *
560  * This class has failities for flipping $USER->id.
561  *
562  * The tear-down method for this class should automatically revert any changes
563  * you make during test set-up using the metods defined here. That is, it will
564  * drop tables for you automatically and revert to the real $DB and $USER->id.
565  *
566  * @package moodlecore
567  * @subpackage simpletestex
568  * @copyright &copy; 2006 The Open University
569  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
570  */
571 class UnitTestCaseUsingDatabase extends UnitTestCase {
572     private $realdb;
573     protected $testdb;
574     private $realuserid = null;
575     private $tables = array();
577     private $realcfg;
578     protected $testcfg;
580     public function __construct($label = false) {
581         global $DB, $CFG;
583         // Complain if we get this far and $CFG->unittestprefix is not set.
584         if (empty($CFG->unittestprefix)) {
585             throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
586         }
588         // Only do this after the above text.
589         parent::UnitTestCase($label);
591         // Create the test DB instance.
592         $this->realdb = $DB;
593         $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
594         $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
596         // Set up test config
597         $this->testcfg = (object)array(
598                 'testcfg' => true, // Marker that this is a test config
599                 'libdir' => $CFG->libdir, // Must use real one so require_once works
600                 'dirroot' => $CFG->dirroot, // Must use real one
601                 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
602                 'ostype' => $CFG->ostype, // Real one
603                 'wwwroot' => 'http://www.example.org', // Use fixed url
604                 'siteadmins' => '0', // No admins
605                 'siteguest' => '0' // No guest
606         );
607         $this->realcfg = $CFG;
608     }
610     /**
611      * Switch to using the test database for all queries until further notice.
612      */
613     protected function switch_to_test_db() {
614         global $DB;
615         if ($DB === $this->testdb) {
616             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);
617         }
618         $DB = $this->testdb;
619     }
621     /**
622      * Revert to using the test database for all future queries.
623      */
624     protected function revert_to_real_db() {
625         global $DB;
626         if ($DB !== $this->testdb) {
627             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);
628         }
629         $DB = $this->realdb;
630     }
632     /**
633      * Switch to using the test $CFG for all queries until further notice.
634      */
635     protected function switch_to_test_cfg() {
636         global $CFG;
637         if (isset($CFG->testcfg)) {
638             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);
639         }
640         $CFG = $this->testcfg;
641     }
643     /**
644      * Revert to using the real $CFG for all future queries.
645      */
646     protected function revert_to_real_cfg() {
647         global $CFG;
648         if (!isset($CFG->testcfg)) {
649             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);
650         }
651         $CFG = $this->realcfg;
652     }
654     /**
655      * Switch $USER->id to a test value.
656      *
657      * It might be worth making this method do more robuse $USER switching in future,
658      * however, this is sufficient for my needs at present.
659      */
660     protected function switch_global_user_id($userid) {
661         global $USER;
662         if (!is_null($this->realuserid)) {
663             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);
664         } else {
665             $this->realuserid = $USER->id;
666         }
667         $USER->id = $userid;
668     }
670     /**
671      * Revert $USER->id to the real value.
672      */
673     protected function revert_global_user_id() {
674         global $USER;
675         if (is_null($this->realuserid)) {
676             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);
677         } else {
678             $USER->id = $this->realuserid;
679             $this->realuserid = null;
680         }
681     }
683     /**
684      * Check that the user has not forgotten to clean anything up, and if they
685      * have, display a rude message and clean it up for them.
686      */
687     private function automatic_clean_up() {
688         global $DB, $CFG;
689         $cleanmore = false;
691         // Drop any test tables that were created.
692         foreach ($this->tables as $tablename => $notused) {
693             $this->drop_test_table($tablename);
694         }
696         // Switch back to the real DB if necessary.
697         if ($DB !== $this->realdb) {
698             $this->revert_to_real_db();
699             $cleanmore = true;
700         }
702         // Switch back to the real CFG if necessary.
703         if (isset($CFG->testcfg)) {
704             $this->revert_to_real_cfg();
705             $cleanmore = true;
706         }
708         // revert_global_user_id if necessary.
709         if (!is_null($this->realuserid)) {
710             $this->revert_global_user_id();
711             $cleanmore = true;
712         }
714         if ($cleanmore) {
715             accesslib_clear_all_caches_for_unit_testing();
716         }
717     }
719     public function tearDown() {
720         $this->automatic_clean_up();
721         parent::tearDown();
722     }
724     public function __destruct() {
725         // Should not be necessary thanks to tearDown, but no harm in belt and braces.
726         $this->automatic_clean_up();
727     }
729     /**
730      * Create a test table just like a real one, getting getting the definition from
731      * the specified install.xml file.
732      * @param string $tablename the name of the test table.
733      * @param string $installxmlfile the install.xml file in which this table is defined.
734      *      $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
735      *      so you need only specify, for example, 'mod/quiz'.
736      */
737     protected function create_test_table($tablename, $installxmlfile) {
738         global $CFG;
739         $dbman = $this->testdb->get_manager();
740         if (isset($this->tables[$tablename])) {
741             debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
742             return;
743         }
744         if ($dbman->table_exists($tablename)) {
745             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);
746             $dbman->drop_table(new xmldb_table($tablename));
747         }
748         $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
749         $this->tables[$tablename] = 1;
750     }
752     /**
753      * Convenience method for calling create_test_table repeatedly.
754      * @param array $tablenames an array of table names.
755      * @param string $installxmlfile the install.xml file in which this table is defined.
756      *      $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
757      *      so you need only specify, for example, 'mod/quiz'.
758      */
759     protected function create_test_tables($tablenames, $installxmlfile) {
760         foreach ($tablenames as $tablename) {
761             $this->create_test_table($tablename, $installxmlfile);
762         }
763     }
765     /**
766      * Drop a test table.
767      * @param $tablename the name of the test table.
768      */
769     protected function drop_test_table($tablename) {
770         if (!isset($this->tables[$tablename])) {
771             debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
772             return;
773         }
774         $dbman = $this->testdb->get_manager();
775         $table = new xmldb_table($tablename);
776         $dbman->drop_table($table);
777         unset($this->tables[$tablename]);
778     }
780     /**
781      * Convenience method for calling drop_test_table repeatedly.
782      * @param array $tablenames an array of table names.
783      */
784     protected function drop_test_tables($tablenames) {
785         foreach ($tablenames as $tablename) {
786             $this->drop_test_table($tablename);
787         }
788     }
790     /**
791      * Load a table with some rows of data. A typical call would look like:
792      *
793      * $config = $this->load_test_data('config_plugins',
794      *         array('plugin', 'name', 'value'), array(
795      *         array('frog', 'numlegs', 2),
796      *         array('frog', 'sound', 'croak'),
797      *         array('frog', 'action', 'jump'),
798      * ));
799      *
800      * @param string $table the table name.
801      * @param array $cols the columns to fill.
802      * @param array $data the data to load.
803      * @return array $objects corresponding to $data.
804      */
805     protected function load_test_data($table, array $cols, array $data) {
806         $results = array();
807         foreach ($data as $rowid => $row) {
808             $obj = new stdClass;
809             foreach ($cols as $key => $colname) {
810                 $obj->$colname = $row[$key];
811             }
812             $obj->id = $this->testdb->insert_record($table, $obj);
813             $results[$rowid] = $obj;
814         }
815         return $results;
816     }
818     /**
819      * Clean up data loaded with load_test_data. The call corresponding to the
820      * example load above would be:
821      *
822      * $this->delete_test_data('config_plugins', $config);
823      *
824      * @param string $table the table name.
825      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
826      */
827     protected function delete_test_data($table, array $rows) {
828         $ids = array();
829         foreach ($rows as $row) {
830             $ids[] = $row->id;
831         }
832         $this->testdb->delete_records_list($table, 'id', $ids);
833     }
837 /**
838  * @package moodlecore
839  * @subpackage simpletestex
840  * @copyright &copy; 2006 The Open University
841  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
842  */
843 class FakeDBUnitTestCase extends UnitTestCase {
844     public $tables = array();
845     public $pkfile;
846     public $cfg;
847     public $DB;
849     /**
850      * In the constructor, record the max(id) of each test table into a csv file.
851      * If this file already exists, it means that a previous run of unit tests
852      * did not complete, and has left data undeleted in the DB. This data is then
853      * deleted and the file is retained. Otherwise it is created.
854      *
855      * throws moodle_exception if CSV file cannot be created
856      */
857     public function __construct($label = false) {
858         global $DB, $CFG;
860         if (empty($CFG->unittestprefix)) {
861             return;
862         }
864         parent::UnitTestCase($label);
865         // MDL-16483 Get PKs and save data to text file
867         $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
868         $this->cfg = $CFG;
870         UnitTestDB::instantiate();
872         $tables = $DB->get_tables();
874         // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
875         if (file_exists($this->pkfile)) {
876             $this->truncate_test_tables($this->get_table_data($this->pkfile));
878         } else { // Create the file
879             $tabledata = '';
881             foreach ($tables as $table) {
882                 if ($table != 'sessions') {
883                     if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
884                         $max_id = 0;
885                     }
886                     $tabledata .= "$table, $max_id\n";
887                 }
888             }
889             if (!file_put_contents($this->pkfile, $tabledata)) {
890                 $a = new stdClass();
891                 $a->filename = $this->pkfile;
892                 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
893             }
894         }
895     }
897     /**
898      * 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.
899      * @param array $tabledata
900      */
901     private function truncate_test_tables($tabledata) {
902         global $CFG, $DB;
904         if (empty($CFG->unittestprefix)) {
905             return;
906         }
908         $tables = $DB->get_tables();
910         foreach ($tables as $table) {
911             if ($table != 'sessions' && isset($tabledata[$table])) {
912                 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
913             }
914         }
915     }
917     /**
918      * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
919      * 1. Table name
920      * 2. Max id
921      *
922      * throws moodle_exception if file doesn't exist
923      *
924      * @param string $filename
925      */
926     public function get_table_data($filename) {
927         global $CFG;
929         if (empty($CFG->unittestprefix)) {
930             return;
931         }
933         if (file_exists($this->pkfile)) {
934             $handle = fopen($this->pkfile, 'r');
935             $tabledata = array();
937             while (($data = fgetcsv($handle, 1000, ",")) !== false) {
938                 $tabledata[$data[0]] = $data[1];
939             }
940             return $tabledata;
941         } else {
942             $a = new stdClass();
943             $a->filename = $this->pkfile;
944             throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
945             return false;
946         }
947     }
949     /**
950      * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
951      * Also detects if this config setting is properly set, and if the user table exists.
952      * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
953      */
954     public function setUp() {
955         global $DB, $CFG;
957         if (empty($CFG->unittestprefix)) {
958             return;
959         }
961         parent::setUp();
962         $this->DB =& $DB;
963         ob_start();
964     }
966     /**
967      * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
968      */
969     public function tearDown() {
970         global $DB, $CFG;
972         if (empty($CFG->unittestprefix)) {
973             return;
974         }
976         if (empty($DB)) {
977             $DB = $this->DB;
978         }
979         $DB->cleanup();
980         parent::tearDown();
982         // Output buffering
983         if (ob_get_length() > 0) {
984             ob_end_flush();
985         }
986     }
988     /**
989      * 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
990      * It should also detect if data is missing from the original tables.
991      */
992     public function __destruct() {
993         global $CFG, $DB;
995         if (empty($CFG->unittestprefix)) {
996             return;
997         }
999         $CFG = $this->cfg;
1000         $this->tearDown();
1001         UnitTestDB::restore();
1002         fulldelete($this->pkfile);
1003     }
1005     /**
1006      * Load a table with some rows of data. A typical call would look like:
1007      *
1008      * $config = $this->load_test_data('config_plugins',
1009      *         array('plugin', 'name', 'value'), array(
1010      *         array('frog', 'numlegs', 2),
1011      *         array('frog', 'sound', 'croak'),
1012      *         array('frog', 'action', 'jump'),
1013      * ));
1014      *
1015      * @param string $table the table name.
1016      * @param array $cols the columns to fill.
1017      * @param array $data the data to load.
1018      * @return array $objects corresponding to $data.
1019      */
1020     public function load_test_data($table, array $cols, array $data) {
1021         global $CFG, $DB;
1023         if (empty($CFG->unittestprefix)) {
1024             return;
1025         }
1027         $results = array();
1028         foreach ($data as $rowid => $row) {
1029             $obj = new stdClass;
1030             foreach ($cols as $key => $colname) {
1031                 $obj->$colname = $row[$key];
1032             }
1033             $obj->id = $DB->insert_record($table, $obj);
1034             $results[$rowid] = $obj;
1035         }
1036         return $results;
1037     }
1039     /**
1040      * Clean up data loaded with load_test_data. The call corresponding to the
1041      * example load above would be:
1042      *
1043      * $this->delete_test_data('config_plugins', $config);
1044      *
1045      * @param string $table the table name.
1046      * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
1047      */
1048     public function delete_test_data($table, array $rows) {
1049         global $CFG, $DB;
1051         if (empty($CFG->unittestprefix)) {
1052             return;
1053         }
1055         $ids = array();
1056         foreach ($rows as $row) {
1057             $ids[] = $row->id;
1058         }
1059         $DB->delete_records_list($table, 'id', $ids);
1060     }
1063 /**
1064  * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
1065  * static instantiate() method, and restores the original global $DB through restore().
1066  * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
1067  * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
1068  * without subclassing it.
1069  *
1070  * @package moodlecore
1071  * @subpackage simpletestex
1072  * @copyright &copy; 2006 The Open University
1073  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1074  */
1075 class UnitTestDB {
1076     public static $DB;
1077     private static $real_db;
1079     public $table_data = array();
1081     /**
1082      * Call this statically to connect to the DB using the unittest prefix, instantiate
1083      * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
1084      */
1085     public static function instantiate() {
1086         global $CFG, $DB;
1087         UnitTestDB::$real_db = clone($DB);
1088         if (empty($CFG->unittestprefix)) {
1089             print_error("prefixnotset", 'simpletest');
1090         }
1092         if (empty(UnitTestDB::$DB)) {
1093             UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
1094             UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
1095         }
1097         $manager = UnitTestDB::$DB->get_manager();
1099         if (!$manager->table_exists('user')) {
1100             print_error('tablesnotsetup', 'simpletest');
1101         }
1103         $DB = new UnitTestDB();
1104     }
1106     public function __call($method, $args) {
1107         // Set args to null if they don't exist (up to 10 args should do)
1108         if (!method_exists($this, $method)) {
1109             return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
1110         } else {
1111             call_user_func_array(array($this, $method), $args);
1112         }
1113     }
1115     public function __get($variable) {
1116         return UnitTestDB::$DB->$variable;
1117     }
1119     public function __set($variable, $value) {
1120         UnitTestDB::$DB->$variable = $value;
1121     }
1123     public function __isset($variable) {
1124         return isset(UnitTestDB::$DB->$variable);
1125     }
1127     public function __unset($variable) {
1128         unset(UnitTestDB::$DB->$variable);
1129     }
1131     /**
1132      * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
1133      */
1134     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1135         global $DB;
1136         $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
1137         $this->table_data[$table][] = $id;
1138         return $id;
1139     }
1141     /**
1142      * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
1143      * throw an exception and cancel update.
1144      *
1145      * throws moodle_exception If trying to update a record not inserted by unit tests.
1146      */
1147     public function update_record($table, $dataobject, $bulk=false) {
1148         global $DB;
1149         if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
1150             // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1151             $a = new stdClass();
1152             $a->id = $dataobject->id;
1153             $a->table = $table;
1154             throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
1155         } else {
1156             return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1157         }
1158     }
1160     /**
1161      * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1162      * throw an exception and cancel delete.
1163      *
1164      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1165      */
1166     public function delete_records($table, array $conditions=array()) {
1167         global $DB;
1168         $tables_to_ignore = array('context_temp');
1170         $a = new stdClass();
1171         $a->table = $table;
1173         // Get ids matching conditions
1174         if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
1175             return UnitTestDB::$DB->delete_records($table, $conditions);
1176         }
1178         $proceed_with_delete = true;
1180         if (!is_array($ids_to_delete)) {
1181             $ids_to_delete = array($ids_to_delete);
1182         }
1184         foreach ($ids_to_delete as $id) {
1185             if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
1186                 $proceed_with_delete = false;
1187                 $a->id = $id;
1188                 break;
1189             }
1190         }
1192         if ($proceed_with_delete) {
1193             return UnitTestDB::$DB->delete_records($table, $conditions);
1194         } else {
1195             throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1196         }
1197     }
1199     /**
1200      * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1201      * throw an exception and cancel delete.
1202      *
1203      * throws moodle_exception If trying to delete a record not inserted by unit tests.
1204      */
1205     public function delete_records_select($table, $select, array $params=null) {
1206         global $DB;
1207         $a = new stdClass();
1208         $a->table = $table;
1210         // Get ids matching conditions
1211         if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1212             return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1213         }
1215         $proceed_with_delete = true;
1217         foreach ($ids_to_delete as $id) {
1218             if (!in_array($id, $this->table_data[$table])) {
1219                 $proceed_with_delete = false;
1220                 $a->id = $id;
1221                 break;
1222             }
1223         }
1225         if ($proceed_with_delete) {
1226             return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1227         } else {
1228             throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1229         }
1230     }
1232     /**
1233      * Removes from the test DB all the records that were inserted during unit tests,
1234      */
1235     public function cleanup() {
1236         global $DB;
1237         foreach ($this->table_data as $table => $ids) {
1238             foreach ($ids as $id) {
1239                 $DB->delete_records($table, array('id' => $id));
1240             }
1241         }
1242     }
1244     /**
1245      * Restores the global $DB object.
1246      */
1247     public static function restore() {
1248         global $DB;
1249         $DB = UnitTestDB::$real_db;
1250     }
1252     public function get_field($table, $return, array $conditions) {
1253         if (!is_array($conditions)) {
1254             throw new coding_exception('$conditions is not an array.');
1255         }
1256         return UnitTestDB::$DB->get_field($table, $return, $conditions);
1257     }