MDL-24060 text_to_html tests. Make sure alt text for LaTeX images doesn't break.
[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 }
208}
8954245a 209/**
210 * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
211 *
212 * @copyright 2009 Tim Hunt
213 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
214 */
d4bc6be7 215class ContainsTagWithAttribute extends XMLStructureExpectation {
8954245a 216 protected $tag;
217 protected $attribute;
218 protected $value;
219
220 function __construct($tag, $attribute, $value, $message = '%s') {
d4bc6be7 221 parent::__construct($message);
8954245a 222 $this->tag = $tag;
223 $this->attribute = $attribute;
224 $this->value = $value;
225 }
226
227 function test($html) {
d4bc6be7 228 $parser = $this->load_xml($html);
6a923128 229 $list = $parser->getElementsByTagName($this->tag);
117bd748 230
6a923128 231 foreach ($list as $node) {
d5bbc449 232 if ($node->attributes->getNamedItem($this->attribute)->nodeValue === (string) $this->value) {
6a923128 233 return true;
234 }
235 }
236 return false;
8954245a 237 }
238
239 function testMessage($html) {
240 return 'Content [' . $html . '] does not contain the tag [' .
241 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
242 }
243}
244
6a923128 245/**
246 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
247 * All attributes must be present and their values must match the expected values.
f8065dd2 248 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
6a923128 249 *
250 * @copyright 2009 Nicolas Connault
251 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
252 */
d4bc6be7 253class ContainsTagWithAttributes extends XMLStructureExpectation {
f8065dd2 254 /**
255 * @var string $tag The name of the Tag to search
256 */
6a923128 257 protected $tag;
f8065dd2 258 /**
259 * @var array $expectedvalues An associative array of parameters, all of which must be matched
260 */
261 protected $expectedvalues = array();
262 /**
263 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
264 */
265 protected $forbiddenvalues = array();
266 /**
267 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
268 */
269 protected $failurereason = 'nomatch';
270
271 function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
d4bc6be7 272 parent::__construct($message);
6a923128 273 $this->tag = $tag;
f8065dd2 274 $this->expectedvalues = $expectedvalues;
275 $this->forbiddenvalues = $forbiddenvalues;
6a923128 276 }
f8065dd2 277
6a923128 278 function test($html) {
d4bc6be7 279 $parser = $this->load_xml($html);
6a923128 280 $list = $parser->getElementsByTagName($this->tag);
f8065dd2 281
282 $foundamatch = false;
283
6a923128 284 // Iterating through inputs
285 foreach ($list as $node) {
286 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
287 continue;
288 }
289
f8065dd2 290 // For the current expected attribute under consideration, check that values match
291 $allattributesmatch = true;
6a923128 292
f8065dd2 293 foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
d5bbc449 294 if ($node->getAttribute($expectedattribute) === '' && $expectedvalue !== '') {
f8065dd2 295 $this->failurereason = 'nomatch';
296 continue 2; // Skip this tag, it doesn't have all the expected attributes
6a923128 297 }
d5bbc449 298 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
f8065dd2 299 $allattributesmatch = false;
300 $this->failurereason = 'nomatch';
6a923128 301 }
302 }
303
f8065dd2 304 if ($allattributesmatch) {
305 $foundamatch = true;
306
307 // Now make sure this node doesn't have any of the forbidden attributes either
308 $nodeattrlist = $node->attributes;
309
310 foreach ($nodeattrlist as $domattrname => $domattr) {
d5bbc449 311 if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) === (string) $this->forbiddenvalues[$domattrname]) {
f8065dd2 312 $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
313 $foundamatch = false;
314 }
315 }
6a923128 316 }
6a923128 317 }
f8065dd2 318
319 return $foundamatch;
6a923128 320 }
f8065dd2 321
6a923128 322 function testMessage($html) {
f8065dd2 323 $output = 'Content [' . $html . '] ';
324
325 if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
326 $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
327 } else if ($this->failurereason == 'nomatch') {
328 $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
329 foreach ($this->expectedvalues as $var => $val) {
330 $output .= "$var=\"$val\" ";
331 }
332 $output = rtrim($output);
333 $output .= '].';
6a923128 334 }
f8065dd2 335
6a923128 336 return $output;
337 }
338}
8954245a 339
340/**
341 * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
342 *
343 * @copyright 2009 Tim Hunt
344 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
345 */
d4bc6be7 346class ContainsTagWithContents extends XMLStructureExpectation {
8954245a 347 protected $tag;
348 protected $content;
349
350 function __construct($tag, $content, $message = '%s') {
d4bc6be7 351 parent::__construct($message);
8954245a 352 $this->tag = $tag;
353 $this->content = $content;
354 }
355
356 function test($html) {
d4bc6be7 357 $parser = $this->load_xml($html);
6a923128 358 $list = $parser->getElementsByTagName($this->tag);
359
360 foreach ($list as $node) {
361 if ($node->textContent == $this->content) {
362 return true;
363 }
364 }
117bd748 365
6a923128 366 return false;
8954245a 367 }
368
369 function testMessage($html) {
370 return 'Content [' . $html . '] does not contain the tag [' .
371 $this->tag . '] with contents [' . $this->content . '].';
6a923128 372 }
373}
374
375/**
376 * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
377 *
378 * @copyright 2009 Nicolas Connault
379 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
380 */
d4bc6be7 381class ContainsEmptyTag extends XMLStructureExpectation {
6a923128 382 protected $tag;
383
384 function __construct($tag, $message = '%s') {
d4bc6be7 385 parent::__construct($message);
6a923128 386 $this->tag = $tag;
387 }
388
389 function test($html) {
d4bc6be7 390 $parser = $this->load_xml($html);
6a923128 391 $list = $parser->getElementsByTagName($this->tag);
392
393 foreach ($list as $node) {
394 if (!$node->hasAttributes() && !$node->hasChildNodes()) {
395 return true;
396 }
397 }
117bd748 398
6a923128 399 return false;
400 }
401
402 function testMessage($html) {
403 return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
8954245a 404 }
405}
406
407
f68cb08b 408/**
409 * This class lets you write unit tests that access a separate set of test
410 * tables with a different prefix. Only those tables you explicitly ask to
411 * be created will be.
240be1d7 412 *
413 * This class has failities for flipping $USER->id.
414 *
415 * The tear-down method for this class should automatically revert any changes
416 * you make during test set-up using the metods defined here. That is, it will
417 * drop tables for you automatically and revert to the real $DB and $USER->id.
b37eac91 418 *
419 * @package moodlecore
420 * @subpackage simpletestex
421 * @copyright &copy; 2006 The Open University
422 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
f68cb08b 423 */
424class UnitTestCaseUsingDatabase extends UnitTestCase {
425 private $realdb;
426 protected $testdb;
82701e24 427 private $realuserid = null;
f68cb08b 428 private $tables = array();
429
2cc9bae5
SM
430 private $realcfg;
431 protected $testcfg;
432
f68cb08b 433 public function __construct($label = false) {
434 global $DB, $CFG;
435
82701e24 436 // Complain if we get this far and $CFG->unittestprefix is not set.
f68cb08b 437 if (empty($CFG->unittestprefix)) {
438 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
439 }
82701e24 440
441 // Only do this after the above text.
f68cb08b 442 parent::UnitTestCase($label);
443
82701e24 444 // Create the test DB instance.
f68cb08b 445 $this->realdb = $DB;
446 $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
447 $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
2cc9bae5
SM
448
449 // Set up test config
450 $this->testcfg = (object)array(
451 'testcfg' => true, // Marker that this is a test config
452 'libdir' => $CFG->libdir, // Must use real one so require_once works
453 'dirroot' => $CFG->dirroot, // Must use real one
454 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
455 'ostype' => $CFG->ostype, // Real one
456 'wwwroot' => 'http://www.example.org', // Use fixed url
457 'siteadmins' => '0', // No admins
458 'siteguest' => '0' // No guest
459 );
460 $this->realcfg = $CFG;
f68cb08b 461 }
462
463 /**
464 * Switch to using the test database for all queries until further notice.
f68cb08b 465 */
466 protected function switch_to_test_db() {
467 global $DB;
468 if ($DB === $this->testdb) {
469 debugging('switch_to_test_db called when the test DB was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
470 }
471 $DB = $this->testdb;
472 }
473
474 /**
475 * Revert to using the test database for all future queries.
476 */
477 protected function revert_to_real_db() {
478 global $DB;
479 if ($DB !== $this->testdb) {
7fa95ef1 480 debugging('revert_to_real_db called when the test DB was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
f68cb08b 481 }
482 $DB = $this->realdb;
483 }
484
2cc9bae5
SM
485 /**
486 * Switch to using the test $CFG for all queries until further notice.
487 */
488 protected function switch_to_test_cfg() {
489 global $CFG;
490 if (isset($CFG->testcfg)) {
491 debugging('switch_to_test_cfg called when the test CFG was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
492 }
493 $CFG = $this->testcfg;
494 }
495
496 /**
497 * Revert to using the real $CFG for all future queries.
498 */
499 protected function revert_to_real_cfg() {
500 global $CFG;
501 if (!isset($CFG->testcfg)) {
502 debugging('revert_to_real_cfg called when the test CFG was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
503 }
504 $CFG = $this->realcfg;
505 }
506
82701e24 507 /**
508 * Switch $USER->id to a test value.
82701e24 509 *
510 * It might be worth making this method do more robuse $USER switching in future,
511 * however, this is sufficient for my needs at present.
512 */
513 protected function switch_global_user_id($userid) {
514 global $USER;
515 if (!is_null($this->realuserid)) {
516 debugging('switch_global_user_id called when $USER->id was already switched to a different value. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
517 } else {
518 $this->realuserid = $USER->id;
519 }
520 $USER->id = $userid;
521 }
522
523 /**
524 * Revert $USER->id to the real value.
525 */
526 protected function revert_global_user_id() {
527 global $USER;
528 if (is_null($this->realuserid)) {
529 debugging('revert_global_user_id called without switch_global_user_id having been called first. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
530 } else {
531 $USER->id = $this->realuserid;
532 $this->realuserid = null;
533 }
534 }
535
f68cb08b 536 /**
537 * Check that the user has not forgotten to clean anything up, and if they
538 * have, display a rude message and clean it up for them.
539 */
240be1d7 540 private function automatic_clean_up() {
ca79c368 541 global $DB, $CFG;
82701e24 542 $cleanmore = false;
f68cb08b 543
240be1d7 544 // Drop any test tables that were created.
f68cb08b 545 foreach ($this->tables as $tablename => $notused) {
546 $this->drop_test_table($tablename);
547 }
548
240be1d7 549 // Switch back to the real DB if necessary.
f68cb08b 550 if ($DB !== $this->realdb) {
f68cb08b 551 $this->revert_to_real_db();
82701e24 552 $cleanmore = true;
553 }
554
2cc9bae5
SM
555 // Switch back to the real CFG if necessary.
556 if (isset($CFG->testcfg)) {
557 $this->revert_to_real_cfg();
558 $cleanmore = true;
559 }
560
240be1d7 561 // revert_global_user_id if necessary.
82701e24 562 if (!is_null($this->realuserid)) {
82701e24 563 $this->revert_global_user_id();
564 $cleanmore = true;
565 }
566
567 if ($cleanmore) {
568 accesslib_clear_all_caches_for_unit_testing();
f68cb08b 569 }
570 }
571
572 public function tearDown() {
240be1d7 573 $this->automatic_clean_up();
f68cb08b 574 parent::tearDown();
575 }
576
577 public function __destruct() {
240be1d7 578 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
579 $this->automatic_clean_up();
f68cb08b 580 }
581
582 /**
583 * Create a test table just like a real one, getting getting the definition from
584 * the specified install.xml file.
585 * @param string $tablename the name of the test table.
586 * @param string $installxmlfile the install.xml file in which this table is defined.
587 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
588 * so you need only specify, for example, 'mod/quiz'.
589 */
590 protected function create_test_table($tablename, $installxmlfile) {
591 global $CFG;
04aa2fd6 592 $dbman = $this->testdb->get_manager();
f68cb08b 593 if (isset($this->tables[$tablename])) {
c39a4607 594 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
f68cb08b 595 return;
596 }
04aa2fd6
EL
597 if ($dbman->table_exists($tablename)) {
598 debugging('This table ' . $tablename . ' already exists from a previous execution. If the error persists you will need to review your code to ensure it is being created only once.', DEBUG_DEVELOPER);
599 $dbman->drop_table(new xmldb_table($tablename));
600 }
c6adec25 601 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
f68cb08b 602 $this->tables[$tablename] = 1;
603 }
604
605 /**
606 * Convenience method for calling create_test_table repeatedly.
607 * @param array $tablenames an array of table names.
608 * @param string $installxmlfile the install.xml file in which this table is defined.
609 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
610 * so you need only specify, for example, 'mod/quiz'.
611 */
612 protected function create_test_tables($tablenames, $installxmlfile) {
613 foreach ($tablenames as $tablename) {
614 $this->create_test_table($tablename, $installxmlfile);
615 }
616 }
617
618 /**
619 * Drop a test table.
620 * @param $tablename the name of the test table.
621 */
622 protected function drop_test_table($tablename) {
623 if (!isset($this->tables[$tablename])) {
624 debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
625 return;
626 }
627 $dbman = $this->testdb->get_manager();
628 $table = new xmldb_table($tablename);
629 $dbman->drop_table($table);
630 unset($this->tables[$tablename]);
631 }
632
633 /**
634 * Convenience method for calling drop_test_table repeatedly.
635 * @param array $tablenames an array of table names.
636 */
637 protected function drop_test_tables($tablenames) {
638 foreach ($tablenames as $tablename) {
639 $this->drop_test_table($tablename);
640 }
641 }
642
643 /**
644 * Load a table with some rows of data. A typical call would look like:
645 *
646 * $config = $this->load_test_data('config_plugins',
647 * array('plugin', 'name', 'value'), array(
648 * array('frog', 'numlegs', 2),
649 * array('frog', 'sound', 'croak'),
650 * array('frog', 'action', 'jump'),
651 * ));
652 *
653 * @param string $table the table name.
654 * @param array $cols the columns to fill.
655 * @param array $data the data to load.
656 * @return array $objects corresponding to $data.
657 */
658 protected function load_test_data($table, array $cols, array $data) {
659 $results = array();
660 foreach ($data as $rowid => $row) {
661 $obj = new stdClass;
662 foreach ($cols as $key => $colname) {
663 $obj->$colname = $row[$key];
664 }
665 $obj->id = $this->testdb->insert_record($table, $obj);
666 $results[$rowid] = $obj;
667 }
668 return $results;
669 }
670
671 /**
672 * Clean up data loaded with load_test_data. The call corresponding to the
673 * example load above would be:
674 *
675 * $this->delete_test_data('config_plugins', $config);
676 *
677 * @param string $table the table name.
678 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
679 */
680 protected function delete_test_data($table, array $rows) {
681 $ids = array();
682 foreach ($rows as $row) {
683 $ids[] = $row->id;
684 }
685 $this->testdb->delete_records_list($table, 'id', $ids);
686 }
687}
688
8954245a 689
b37eac91 690/**
691 * @package moodlecore
692 * @subpackage simpletestex
693 * @copyright &copy; 2006 The Open University
694 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
695 */
58fa5d6f 696class FakeDBUnitTestCase extends UnitTestCase {
356e0010 697 public $tables = array();
90997d6d 698 public $pkfile;
699 public $cfg;
274e2947 700 public $DB;
356e0010 701
90997d6d 702 /**
703 * In the constructor, record the max(id) of each test table into a csv file.
704 * If this file already exists, it means that a previous run of unit tests
705 * did not complete, and has left data undeleted in the DB. This data is then
706 * deleted and the file is retained. Otherwise it is created.
b37eac91 707 *
708 * throws moodle_exception if CSV file cannot be created
90997d6d 709 */
b9c639d6 710 public function __construct($label = false) {
84ebf08d 711 global $DB, $CFG;
712
713 if (empty($CFG->unittestprefix)) {
714 return;
715 }
716
b9c639d6 717 parent::UnitTestCase($label);
90997d6d 718 // MDL-16483 Get PKs and save data to text file
84ebf08d 719
90997d6d 720 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
721 $this->cfg = $CFG;
274e2947 722
723 UnitTestDB::instantiate();
90997d6d 724
725 $tables = $DB->get_tables();
726
727 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
728 if (file_exists($this->pkfile)) {
729 $this->truncate_test_tables($this->get_table_data($this->pkfile));
730
731 } else { // Create the file
732 $tabledata = '';
733
734 foreach ($tables as $table) {
58fa5d6f 735 if ($table != 'sessions') {
90997d6d 736 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
737 $max_id = 0;
738 }
739 $tabledata .= "$table, $max_id\n";
740 }
741 }
742 if (!file_put_contents($this->pkfile, $tabledata)) {
5d1381c2 743 $a = new stdClass();
744 $a->filename = $this->pkfile;
745 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
90997d6d 746 }
747 }
748 }
749
750 /**
751 * Given an array of tables and their max id, truncates all test table records whose id is higher than the ones in the $tabledata array.
752 * @param array $tabledata
753 */
754 private function truncate_test_tables($tabledata) {
755 global $CFG, $DB;
756
84ebf08d 757 if (empty($CFG->unittestprefix)) {
758 return;
759 }
760
90997d6d 761 $tables = $DB->get_tables();
762
763 foreach ($tables as $table) {
3f57bd45 764 if ($table != 'sessions' && isset($tabledata[$table])) {
5d1381c2 765 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
90997d6d 766 }
767 }
356e0010 768 }
769
90997d6d 770 /**
771 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
772 * 1. Table name
773 * 2. Max id
b37eac91 774 *
775 * throws moodle_exception if file doesn't exist
776 *
90997d6d 777 * @param string $filename
90997d6d 778 */
779 public function get_table_data($filename) {
84ebf08d 780 global $CFG;
781
782 if (empty($CFG->unittestprefix)) {
783 return;
784 }
785
90997d6d 786 if (file_exists($this->pkfile)) {
787 $handle = fopen($this->pkfile, 'r');
788 $tabledata = array();
789
790 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
791 $tabledata[$data[0]] = $data[1];
792 }
793 return $tabledata;
794 } else {
5d1381c2 795 $a = new stdClass();
796 $a->filename = $this->pkfile;
5d1381c2 797 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
90997d6d 798 return false;
799 }
800 }
801
802 /**
803 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
804 * Also detects if this config setting is properly set, and if the user table exists.
b37eac91 805 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
90997d6d 806 */
356e0010 807 public function setUp() {
84ebf08d 808 global $DB, $CFG;
809
810 if (empty($CFG->unittestprefix)) {
811 return;
812 }
813
356e0010 814 parent::setUp();
274e2947 815 $this->DB =& $DB;
915d745f 816 ob_start();
5d1381c2 817 }
818
819 /**
820 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
821 */
822 public function tearDown() {
84ebf08d 823 global $DB, $CFG;
824
825 if (empty($CFG->unittestprefix)) {
826 return;
827 }
828
274e2947 829 if (empty($DB)) {
830 $DB = $this->DB;
831 }
5d1381c2 832 $DB->cleanup();
833 parent::tearDown();
747f4a4c 834
835 // Output buffering
915d745f 836 if (ob_get_length() > 0) {
837 ob_end_flush();
838 }
5d1381c2 839 }
840
841 /**
842 * This will execute once all the tests have been run. It should delete the text file holding info about database contents prior to the tests
843 * It should also detect if data is missing from the original tables.
844 */
845 public function __destruct() {
846 global $CFG, $DB;
847
84ebf08d 848 if (empty($CFG->unittestprefix)) {
849 return;
850 }
851
5d1381c2 852 $CFG = $this->cfg;
853 $this->tearDown();
854 UnitTestDB::restore();
855 fulldelete($this->pkfile);
856 }
821e4ecf 857
858 /**
859 * Load a table with some rows of data. A typical call would look like:
860 *
861 * $config = $this->load_test_data('config_plugins',
862 * array('plugin', 'name', 'value'), array(
863 * array('frog', 'numlegs', 2),
864 * array('frog', 'sound', 'croak'),
865 * array('frog', 'action', 'jump'),
866 * ));
867 *
868 * @param string $table the table name.
869 * @param array $cols the columns to fill.
870 * @param array $data the data to load.
871 * @return array $objects corresponding to $data.
872 */
873 public function load_test_data($table, array $cols, array $data) {
84ebf08d 874 global $CFG, $DB;
875
876 if (empty($CFG->unittestprefix)) {
877 return;
878 }
879
821e4ecf 880 $results = array();
881 foreach ($data as $rowid => $row) {
882 $obj = new stdClass;
883 foreach ($cols as $key => $colname) {
884 $obj->$colname = $row[$key];
885 }
886 $obj->id = $DB->insert_record($table, $obj);
887 $results[$rowid] = $obj;
888 }
889 return $results;
890 }
891
892 /**
893 * Clean up data loaded with load_test_data. The call corresponding to the
894 * example load above would be:
895 *
896 * $this->delete_test_data('config_plugins', $config);
897 *
898 * @param string $table the table name.
899 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
900 */
901 public function delete_test_data($table, array $rows) {
84ebf08d 902 global $CFG, $DB;
903
904 if (empty($CFG->unittestprefix)) {
905 return;
906 }
907
821e4ecf 908 $ids = array();
909 foreach ($rows as $row) {
910 $ids[] = $row->id;
911 }
912 $DB->delete_records_list($table, 'id', $ids);
913 }
5d1381c2 914}
915
916/**
917 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
918 * static instantiate() method, and restores the original global $DB through restore().
919 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
920 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
921 * without subclassing it.
b37eac91 922 *
923 * @package moodlecore
924 * @subpackage simpletestex
925 * @copyright &copy; 2006 The Open University
926 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5d1381c2 927 */
928class UnitTestDB {
929 public static $DB;
930 private static $real_db;
356e0010 931
5d1381c2 932 public $table_data = array();
933
5d1381c2 934 /**
935 * Call this statically to connect to the DB using the unittest prefix, instantiate
936 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
937 */
938 public static function instantiate() {
939 global $CFG, $DB;
940 UnitTestDB::$real_db = clone($DB);
90997d6d 941 if (empty($CFG->unittestprefix)) {
b9c639d6 942 print_error("prefixnotset", 'simpletest');
943 }
944
274e2947 945 if (empty(UnitTestDB::$DB)) {
946 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
beaa43db 947 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
274e2947 948 }
949
5d1381c2 950 $manager = UnitTestDB::$DB->get_manager();
356e0010 951
952 if (!$manager->table_exists('user')) {
b9c639d6 953 print_error('tablesnotsetup', 'simpletest');
954 }
5d1381c2 955
956 $DB = new UnitTestDB();
957 }
958
959 public function __call($method, $args) {
960 // Set args to null if they don't exist (up to 10 args should do)
961 if (!method_exists($this, $method)) {
962 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
963 } else {
964 call_user_func_array(array($this, $method), $args);
965 }
966 }
967
968 public function __get($variable) {
969 return UnitTestDB::$DB->$variable;
970 }
971
972 public function __set($variable, $value) {
973 UnitTestDB::$DB->$variable = $value;
974 }
975
976 public function __isset($variable) {
977 return isset(UnitTestDB::$DB->$variable);
978 }
979
980 public function __unset($variable) {
981 unset(UnitTestDB::$DB->$variable);
b9c639d6 982 }
983
90997d6d 984 /**
5d1381c2 985 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
90997d6d 986 */
5d1381c2 987 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
356e0010 988 global $DB;
5d1381c2 989 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
990 $this->table_data[$table][] = $id;
991 return $id;
992 }
356e0010 993
5d1381c2 994 /**
995 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
996 * throw an exception and cancel update.
117bd748 997 *
b37eac91 998 * throws moodle_exception If trying to update a record not inserted by unit tests.
5d1381c2 999 */
1000 public function update_record($table, $dataobject, $bulk=false) {
1001 global $DB;
747f4a4c 1002 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
274e2947 1003 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1004 $a = new stdClass();
1005 $a->id = $dataobject->id;
1006 $a->table = $table;
274e2947 1007 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
5d1381c2 1008 } else {
1009 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1010 }
b9c639d6 1011 }
1012
1013 /**
5d1381c2 1014 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1015 * throw an exception and cancel delete.
b37eac91 1016 *
1017 * throws moodle_exception If trying to delete a record not inserted by unit tests.
b9c639d6 1018 */
274e2947 1019 public function delete_records($table, array $conditions=array()) {
5d1381c2 1020 global $DB;
747f4a4c 1021 $tables_to_ignore = array('context_temp');
1022
5d1381c2 1023 $a = new stdClass();
1024 $a->table = $table;
1025
1026 // Get ids matching conditions
1027 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
1028 return UnitTestDB::$DB->delete_records($table, $conditions);
1029 }
1030
1031 $proceed_with_delete = true;
1032
1033 if (!is_array($ids_to_delete)) {
1034 $ids_to_delete = array($ids_to_delete);
1035 }
1036
1037 foreach ($ids_to_delete as $id) {
747f4a4c 1038 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
5d1381c2 1039 $proceed_with_delete = false;
1040 $a->id = $id;
1041 break;
1042 }
1043 }
1044
1045 if ($proceed_with_delete) {
1046 return UnitTestDB::$DB->delete_records($table, $conditions);
1047 } else {
5d1381c2 1048 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1049 }
b9c639d6 1050 }
b9c639d6 1051
5d1381c2 1052 /**
1053 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1054 * throw an exception and cancel delete.
b37eac91 1055 *
1056 * throws moodle_exception If trying to delete a record not inserted by unit tests.
5d1381c2 1057 */
1058 public function delete_records_select($table, $select, array $params=null) {
1059 global $DB;
1060 $a = new stdClass();
1061 $a->table = $table;
1062
1063 // Get ids matching conditions
1064 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1065 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1066 }
1067
1068 $proceed_with_delete = true;
1069
1070 foreach ($ids_to_delete as $id) {
1071 if (!in_array($id, $this->table_data[$table])) {
1072 $proceed_with_delete = false;
1073 $a->id = $id;
1074 break;
1075 }
1076 }
1077
1078 if ($proceed_with_delete) {
1079 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1080 } else {
5d1381c2 1081 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1082 }
1083 }
1084
1085 /**
1086 * Removes from the test DB all the records that were inserted during unit tests,
1087 */
1088 public function cleanup() {
1089 global $DB;
1090 foreach ($this->table_data as $table => $ids) {
1091 foreach ($ids as $id) {
1092 $DB->delete_records($table, array('id' => $id));
1093 }
1094 }
1095 }
1096
1097 /**
1098 * Restores the global $DB object.
1099 */
1100 public static function restore() {
1101 global $DB;
1102 $DB = UnitTestDB::$real_db;
1103 }
1104
1105 public function get_field($table, $return, array $conditions) {
1106 if (!is_array($conditions)) {
fa98e6d1 1107 throw new coding_exception('$conditions is not an array.');
5d1381c2 1108 }
1109 return UnitTestDB::$DB->get_field($table, $return, $conditions);
1110 }
1111}