MDL-25715 simpletestlib new expectations for testing renderer output.
[moodle.git] / lib / simpletestlib.php
CommitLineData
b37eac91 1<?php
2
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/>.
17
3ef8c936 18/**
19 * Utility functions to make unit testing easier.
b9c639d6 20 *
3ef8c936 21 * These functions, particularly the the database ones, are quick and
b9c639d6 22 * dirty methods for getting things done in test cases. None of these
3ef8c936 23 * methods should be used outside test code.
24 *
b37eac91 25 * Major Contirbutors
26 * - T.J.Hunt@open.ac.uk
27 *
78bfb562 28 * @package core
b37eac91 29 * @subpackage simpletestex
78bfb562
PS
30 * @copyright &copy; 2006 The Open University
31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3ef8c936 32 */
33
78bfb562
PS
34defined('MOODLE_INTERNAL') || die();
35
b37eac91 36/**
37 * Includes
38 */
3ef8c936 39require_once(dirname(__FILE__) . '/../config.php');
40require_once($CFG->libdir . '/simpletestlib/simpletest.php');
41require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
42require_once($CFG->libdir . '/simpletestlib/expectation.php');
4309bd17 43require_once($CFG->libdir . '/simpletestlib/reporter.php');
a205dcdc 44require_once($CFG->libdir . '/simpletestlib/web_tester.php');
76144765 45require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
3ef8c936 46
47/**
48 * Recursively visit all the files in the source tree. Calls the callback
b9c639d6 49 * function with the pathname of each file found.
50 *
51 * @param $path the folder to start searching from.
3ef8c936 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).
b9c639d6 54 * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
3ef8c936 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).
b9c639d6 57 */
3ef8c936 58function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
59 $files = scandir($path);
60
61 foreach ($files as $file) {
62 $filepath = $path .'/'. $file;
2e710be8 63 if (strpos($file, '.') === 0) {
64 /// Don't check hidden files.
3ef8c936 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}
75
76/**
77 * An expectation for comparing strings ignoring whitespace.
b37eac91 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
3ef8c936 83 */
84class IgnoreWhitespaceExpectation extends SimpleExpectation {
85 var $expect;
86
87 function IgnoreWhitespaceExpectation($content, $message = '%s') {
88 $this->SimpleExpectation($message);
89 $this->expect=$this->normalise($content);
90 }
91
92 function test($ip) {
93 return $this->normalise($ip)==$this->expect;
94 }
95
96 function normalise($text) {
97 return preg_replace('/\s+/m',' ',trim($text));
98 }
99
100 function testMessage($ip) {
101 return "Input string [$ip] doesn't match the required value.";
102 }
103}
104
105/**
106 * An Expectation that two arrays contain the same list of values.
b37eac91 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
3ef8c936 112 */
113class ArraysHaveSameValuesExpectation extends SimpleExpectation {
114 var $expect;
115
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 }
124
125 function test($actual) {
126 return $this->normalise($actual) == $this->expect;
127 }
128
129 function normalise($array) {
130 sort($array);
131 return $array;
132 }
133
134 function testMessage($actual) {
135 return 'Array [' . implode(', ', $actual) .
136 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
137 }
138}
139
8954245a 140
3ef8c936 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.)
b37eac91 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
3ef8c936 150 */
151class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
152 var $expect;
153
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 }
162
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 }
175
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 {
3efb762e 184 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
3ef8c936 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 }
190}
191
d4bc6be7 192abstract class XMLStructureExpectation extends SimpleExpectation {
4454447d
PS
193 /**
194 * Parse a string as XML and return a DOMDocument;
195 * @param $html
196 * @return unknown_type
197 */
d4bc6be7 198 protected function load_xml($html) {
4454447d
PS
199 $prevsetting = libxml_use_internal_errors(true);
200 $parser = new DOMDocument();
201 if (!$parser->loadXML('<html>' . $html . '</html>')) {
202 $parser = new DOMDocument();
203 }
3d39d775 204 libxml_clear_errors();
4454447d
PS
205 libxml_use_internal_errors($prevsetting);
206 return $parser;
d4bc6be7 207 }
ccdd1596
TH
208
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 }
d4bc6be7 220}
8954245a 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 */
d4bc6be7 227class ContainsTagWithAttribute extends XMLStructureExpectation {
8954245a 228 protected $tag;
229 protected $attribute;
230 protected $value;
231
232 function __construct($tag, $attribute, $value, $message = '%s') {
d4bc6be7 233 parent::__construct($message);
8954245a 234 $this->tag = $tag;
235 $this->attribute = $attribute;
236 $this->value = $value;
237 }
238
239 function test($html) {
d4bc6be7 240 $parser = $this->load_xml($html);
ccdd1596
TH
241 if (is_array($parser)) {
242 return false;
243 }
6a923128 244 $list = $parser->getElementsByTagName($this->tag);
117bd748 245
6a923128 246 foreach ($list as $node) {
d5bbc449 247 if ($node->attributes->getNamedItem($this->attribute)->nodeValue === (string) $this->value) {
6a923128 248 return true;
249 }
250 }
251 return false;
8954245a 252 }
253
ccdd1596 254 function customMessage($html) {
8954245a 255 return 'Content [' . $html . '] does not contain the tag [' .
256 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
257 }
258}
259
6a923128 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.
f8065dd2 263 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
6a923128 264 *
265 * @copyright 2009 Nicolas Connault
266 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
267 */
d4bc6be7 268class ContainsTagWithAttributes extends XMLStructureExpectation {
f8065dd2 269 /**
270 * @var string $tag The name of the Tag to search
271 */
6a923128 272 protected $tag;
f8065dd2 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';
285
286 function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
d4bc6be7 287 parent::__construct($message);
6a923128 288 $this->tag = $tag;
f8065dd2 289 $this->expectedvalues = $expectedvalues;
290 $this->forbiddenvalues = $forbiddenvalues;
6a923128 291 }
f8065dd2 292
6a923128 293 function test($html) {
d4bc6be7 294 $parser = $this->load_xml($html);
ccdd1596
TH
295 if (is_array($parser)) {
296 return false;
297 }
f8065dd2 298
ccdd1596 299 $list = $parser->getElementsByTagName($this->tag);
f8065dd2 300 $foundamatch = false;
301
6a923128 302 // Iterating through inputs
303 foreach ($list as $node) {
304 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
305 continue;
306 }
307
f8065dd2 308 // For the current expected attribute under consideration, check that values match
309 $allattributesmatch = true;
6a923128 310
f8065dd2 311 foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
d5bbc449 312 if ($node->getAttribute($expectedattribute) === '' && $expectedvalue !== '') {
f8065dd2 313 $this->failurereason = 'nomatch';
314 continue 2; // Skip this tag, it doesn't have all the expected attributes
6a923128 315 }
d5bbc449 316 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
f8065dd2 317 $allattributesmatch = false;
318 $this->failurereason = 'nomatch';
6a923128 319 }
320 }
321
f8065dd2 322 if ($allattributesmatch) {
323 $foundamatch = true;
324
325 // Now make sure this node doesn't have any of the forbidden attributes either
326 $nodeattrlist = $node->attributes;
327
328 foreach ($nodeattrlist as $domattrname => $domattr) {
d5bbc449 329 if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) === (string) $this->forbiddenvalues[$domattrname]) {
f8065dd2 330 $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
331 $foundamatch = false;
332 }
333 }
6a923128 334 }
6a923128 335 }
f8065dd2 336
337 return $foundamatch;
6a923128 338 }
f8065dd2 339
ccdd1596 340 function customMessage($html) {
f8065dd2 341 $output = 'Content [' . $html . '] ';
342
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 .= '].';
6a923128 352 }
f8065dd2 353
6a923128 354 return $output;
355 }
356}
8954245a 357
ccdd1596
TH
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 */
366class 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;
383
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 }
391
392 function test($html) {
393 $parser = $this->load_xml($html);
394 if (is_array($parser)) {
395 return false;
396 }
397
398 $list = $parser->getElementsByTagName('select');
399
400 // Iterating through inputs
401 foreach ($list as $node) {
402 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
403 continue;
404 }
405
406 if ($node->getAttribute('name') != $this->name) {
407 continue;
408 }
409
410 if ($this->enabled === true && $node->getAttribute('disabled')) {
411 continue;
412 } else if ($this->enabled === false && $node->getAttribute('disabled') != 'disabled') {
413 continue;
414 }
415
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 }
439
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 }
454
455 return $output;
456 }
457}
458
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 */
466class 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 . '] ';
475
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 .= '].';
482
483 return $output;
484 }
485}
486
8954245a 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 */
d4bc6be7 493class ContainsTagWithContents extends XMLStructureExpectation {
8954245a 494 protected $tag;
495 protected $content;
496
497 function __construct($tag, $content, $message = '%s') {
d4bc6be7 498 parent::__construct($message);
8954245a 499 $this->tag = $tag;
500 $this->content = $content;
501 }
502
503 function test($html) {
d4bc6be7 504 $parser = $this->load_xml($html);
6a923128 505 $list = $parser->getElementsByTagName($this->tag);
506
507 foreach ($list as $node) {
508 if ($node->textContent == $this->content) {
509 return true;
510 }
511 }
117bd748 512
6a923128 513 return false;
8954245a 514 }
515
516 function testMessage($html) {
517 return 'Content [' . $html . '] does not contain the tag [' .
518 $this->tag . '] with contents [' . $this->content . '].';
6a923128 519 }
520}
521
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 */
d4bc6be7 528class ContainsEmptyTag extends XMLStructureExpectation {
6a923128 529 protected $tag;
530
531 function __construct($tag, $message = '%s') {
d4bc6be7 532 parent::__construct($message);
6a923128 533 $this->tag = $tag;
534 }
535
536 function test($html) {
d4bc6be7 537 $parser = $this->load_xml($html);
6a923128 538 $list = $parser->getElementsByTagName($this->tag);
539
540 foreach ($list as $node) {
541 if (!$node->hasAttributes() && !$node->hasChildNodes()) {
542 return true;
543 }
544 }
117bd748 545
6a923128 546 return false;
547 }
548
549 function testMessage($html) {
550 return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
8954245a 551 }
552}
553
554
f68cb08b 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.
240be1d7 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.
b37eac91 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
f68cb08b 570 */
571class UnitTestCaseUsingDatabase extends UnitTestCase {
572 private $realdb;
573 protected $testdb;
82701e24 574 private $realuserid = null;
f68cb08b 575 private $tables = array();
576
2cc9bae5
SM
577 private $realcfg;
578 protected $testcfg;
579
f68cb08b 580 public function __construct($label = false) {
581 global $DB, $CFG;
582
82701e24 583 // Complain if we get this far and $CFG->unittestprefix is not set.
f68cb08b 584 if (empty($CFG->unittestprefix)) {
585 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
586 }
82701e24 587
588 // Only do this after the above text.
f68cb08b 589 parent::UnitTestCase($label);
590
82701e24 591 // Create the test DB instance.
f68cb08b 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);
2cc9bae5
SM
595
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;
f68cb08b 608 }
609
610 /**
611 * Switch to using the test database for all queries until further notice.
f68cb08b 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 }
620
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) {
7fa95ef1 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);
f68cb08b 628 }
629 $DB = $this->realdb;
630 }
631
2cc9bae5
SM
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 }
642
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 }
653
82701e24 654 /**
655 * Switch $USER->id to a test value.
82701e24 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 }
669
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 }
682
f68cb08b 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 */
240be1d7 687 private function automatic_clean_up() {
ca79c368 688 global $DB, $CFG;
82701e24 689 $cleanmore = false;
f68cb08b 690
240be1d7 691 // Drop any test tables that were created.
f68cb08b 692 foreach ($this->tables as $tablename => $notused) {
693 $this->drop_test_table($tablename);
694 }
695
240be1d7 696 // Switch back to the real DB if necessary.
f68cb08b 697 if ($DB !== $this->realdb) {
f68cb08b 698 $this->revert_to_real_db();
82701e24 699 $cleanmore = true;
700 }
701
2cc9bae5
SM
702 // Switch back to the real CFG if necessary.
703 if (isset($CFG->testcfg)) {
704 $this->revert_to_real_cfg();
705 $cleanmore = true;
706 }
707
240be1d7 708 // revert_global_user_id if necessary.
82701e24 709 if (!is_null($this->realuserid)) {
82701e24 710 $this->revert_global_user_id();
711 $cleanmore = true;
712 }
713
714 if ($cleanmore) {
715 accesslib_clear_all_caches_for_unit_testing();
f68cb08b 716 }
717 }
718
719 public function tearDown() {
240be1d7 720 $this->automatic_clean_up();
f68cb08b 721 parent::tearDown();
722 }
723
724 public function __destruct() {
240be1d7 725 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
726 $this->automatic_clean_up();
f68cb08b 727 }
728
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;
04aa2fd6 739 $dbman = $this->testdb->get_manager();
f68cb08b 740 if (isset($this->tables[$tablename])) {
c39a4607 741 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
f68cb08b 742 return;
743 }
04aa2fd6
EL
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 }
c6adec25 748 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
f68cb08b 749 $this->tables[$tablename] = 1;
750 }
751
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 }
764
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 }
779
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 }
789
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 }
817
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 }
834}
835
8954245a 836
b37eac91 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 */
58fa5d6f 843class FakeDBUnitTestCase extends UnitTestCase {
356e0010 844 public $tables = array();
90997d6d 845 public $pkfile;
846 public $cfg;
274e2947 847 public $DB;
356e0010 848
90997d6d 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.
b37eac91 854 *
855 * throws moodle_exception if CSV file cannot be created
90997d6d 856 */
b9c639d6 857 public function __construct($label = false) {
84ebf08d 858 global $DB, $CFG;
859
860 if (empty($CFG->unittestprefix)) {
861 return;
862 }
863
b9c639d6 864 parent::UnitTestCase($label);
90997d6d 865 // MDL-16483 Get PKs and save data to text file
84ebf08d 866
90997d6d 867 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
868 $this->cfg = $CFG;
274e2947 869
870 UnitTestDB::instantiate();
90997d6d 871
872 $tables = $DB->get_tables();
873
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));
877
878 } else { // Create the file
879 $tabledata = '';
880
881 foreach ($tables as $table) {
58fa5d6f 882 if ($table != 'sessions') {
90997d6d 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)) {
5d1381c2 890 $a = new stdClass();
891 $a->filename = $this->pkfile;
892 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
90997d6d 893 }
894 }
895 }
896
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;
903
84ebf08d 904 if (empty($CFG->unittestprefix)) {
905 return;
906 }
907
90997d6d 908 $tables = $DB->get_tables();
909
910 foreach ($tables as $table) {
3f57bd45 911 if ($table != 'sessions' && isset($tabledata[$table])) {
5d1381c2 912 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
90997d6d 913 }
914 }
356e0010 915 }
916
90997d6d 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
b37eac91 921 *
922 * throws moodle_exception if file doesn't exist
923 *
90997d6d 924 * @param string $filename
90997d6d 925 */
926 public function get_table_data($filename) {
84ebf08d 927 global $CFG;
928
929 if (empty($CFG->unittestprefix)) {
930 return;
931 }
932
90997d6d 933 if (file_exists($this->pkfile)) {
934 $handle = fopen($this->pkfile, 'r');
935 $tabledata = array();
936
937 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
938 $tabledata[$data[0]] = $data[1];
939 }
940 return $tabledata;
941 } else {
5d1381c2 942 $a = new stdClass();
943 $a->filename = $this->pkfile;
5d1381c2 944 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
90997d6d 945 return false;
946 }
947 }
948
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.
b37eac91 952 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
90997d6d 953 */
356e0010 954 public function setUp() {
84ebf08d 955 global $DB, $CFG;
956
957 if (empty($CFG->unittestprefix)) {
958 return;
959 }
960
356e0010 961 parent::setUp();
274e2947 962 $this->DB =& $DB;
915d745f 963 ob_start();
5d1381c2 964 }
965
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() {
84ebf08d 970 global $DB, $CFG;
971
972 if (empty($CFG->unittestprefix)) {
973 return;
974 }
975
274e2947 976 if (empty($DB)) {
977 $DB = $this->DB;
978 }
5d1381c2 979 $DB->cleanup();
980 parent::tearDown();
747f4a4c 981
982 // Output buffering
915d745f 983 if (ob_get_length() > 0) {
984 ob_end_flush();
985 }
5d1381c2 986 }
987
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;
994
84ebf08d 995 if (empty($CFG->unittestprefix)) {
996 return;
997 }
998
5d1381c2 999 $CFG = $this->cfg;
1000 $this->tearDown();
1001 UnitTestDB::restore();
1002 fulldelete($this->pkfile);
1003 }
821e4ecf 1004
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) {
84ebf08d 1021 global $CFG, $DB;
1022
1023 if (empty($CFG->unittestprefix)) {
1024 return;
1025 }
1026
821e4ecf 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 }
1038
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) {
84ebf08d 1049 global $CFG, $DB;
1050
1051 if (empty($CFG->unittestprefix)) {
1052 return;
1053 }
1054
821e4ecf 1055 $ids = array();
1056 foreach ($rows as $row) {
1057 $ids[] = $row->id;
1058 }
1059 $DB->delete_records_list($table, 'id', $ids);
1060 }
5d1381c2 1061}
1062
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.
b37eac91 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
5d1381c2 1074 */
1075class UnitTestDB {
1076 public static $DB;
1077 private static $real_db;
356e0010 1078
5d1381c2 1079 public $table_data = array();
1080
5d1381c2 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);
90997d6d 1088 if (empty($CFG->unittestprefix)) {
b9c639d6 1089 print_error("prefixnotset", 'simpletest');
1090 }
1091
274e2947 1092 if (empty(UnitTestDB::$DB)) {
1093 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
beaa43db 1094 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
274e2947 1095 }
1096
5d1381c2 1097 $manager = UnitTestDB::$DB->get_manager();
356e0010 1098
1099 if (!$manager->table_exists('user')) {
b9c639d6 1100 print_error('tablesnotsetup', 'simpletest');
1101 }
5d1381c2 1102
1103 $DB = new UnitTestDB();
1104 }
1105
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 }
1114
1115 public function __get($variable) {
1116 return UnitTestDB::$DB->$variable;
1117 }
1118
1119 public function __set($variable, $value) {
1120 UnitTestDB::$DB->$variable = $value;
1121 }
1122
1123 public function __isset($variable) {
1124 return isset(UnitTestDB::$DB->$variable);
1125 }
1126
1127 public function __unset($variable) {
1128 unset(UnitTestDB::$DB->$variable);
b9c639d6 1129 }
1130
90997d6d 1131 /**
5d1381c2 1132 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
90997d6d 1133 */
5d1381c2 1134 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
356e0010 1135 global $DB;
5d1381c2 1136 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
1137 $this->table_data[$table][] = $id;
1138 return $id;
1139 }
356e0010 1140
5d1381c2 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.
117bd748 1144 *
b37eac91 1145 * throws moodle_exception If trying to update a record not inserted by unit tests.
5d1381c2 1146 */
1147 public function update_record($table, $dataobject, $bulk=false) {
1148 global $DB;
747f4a4c 1149 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
274e2947 1150 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1151 $a = new stdClass();
1152 $a->id = $dataobject->id;
1153 $a->table = $table;
274e2947 1154 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
5d1381c2 1155 } else {
1156 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1157 }
b9c639d6 1158 }
1159
1160 /**
5d1381c2 1161 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1162 * throw an exception and cancel delete.
b37eac91 1163 *
1164 * throws moodle_exception If trying to delete a record not inserted by unit tests.
b9c639d6 1165 */
274e2947 1166 public function delete_records($table, array $conditions=array()) {
5d1381c2 1167 global $DB;
747f4a4c 1168 $tables_to_ignore = array('context_temp');
1169
5d1381c2 1170 $a = new stdClass();
1171 $a->table = $table;
1172
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 }
1177
1178 $proceed_with_delete = true;
1179
1180 if (!is_array($ids_to_delete)) {
1181 $ids_to_delete = array($ids_to_delete);
1182 }
1183
1184 foreach ($ids_to_delete as $id) {
747f4a4c 1185 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
5d1381c2 1186 $proceed_with_delete = false;
1187 $a->id = $id;
1188 break;
1189 }
1190 }
1191
1192 if ($proceed_with_delete) {
1193 return UnitTestDB::$DB->delete_records($table, $conditions);
1194 } else {
5d1381c2 1195 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1196 }
b9c639d6 1197 }
b9c639d6 1198
5d1381c2 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.
b37eac91 1202 *
1203 * throws moodle_exception If trying to delete a record not inserted by unit tests.
5d1381c2 1204 */
1205 public function delete_records_select($table, $select, array $params=null) {
1206 global $DB;
1207 $a = new stdClass();
1208 $a->table = $table;
1209
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 }
1214
1215 $proceed_with_delete = true;
1216
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 }
1224
1225 if ($proceed_with_delete) {
1226 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1227 } else {
5d1381c2 1228 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1229 }
1230 }
1231
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 }
1243
1244 /**
1245 * Restores the global $DB object.
1246 */
1247 public static function restore() {
1248 global $DB;
1249 $DB = UnitTestDB::$real_db;
1250 }
1251
1252 public function get_field($table, $return, array $conditions) {
1253 if (!is_array($conditions)) {
fa98e6d1 1254 throw new coding_exception('$conditions is not an array.');
5d1381c2 1255 }
1256 return UnitTestDB::$DB->get_field($table, $return, $conditions);
1257 }
1258}