MDL-20787 final switch to new $OUTPUT->pix_url() - yay!
[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 *
28 * @package moodlecore
29 * @subpackage simpletestex
3ef8c936 30 * @copyright &copy; 2006 The Open University
b37eac91 31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3ef8c936 32 */
33
b37eac91 34/**
35 * Includes
36 */
3ef8c936 37require_once(dirname(__FILE__) . '/../config.php');
38require_once($CFG->libdir . '/simpletestlib/simpletest.php');
39require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
40require_once($CFG->libdir . '/simpletestlib/expectation.php');
4309bd17 41require_once($CFG->libdir . '/simpletestlib/reporter.php');
a205dcdc 42require_once($CFG->libdir . '/simpletestlib/web_tester.php');
76144765 43require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
3ef8c936 44
45/**
46 * Recursively visit all the files in the source tree. Calls the callback
b9c639d6 47 * function with the pathname of each file found.
48 *
49 * @param $path the folder to start searching from.
3ef8c936 50 * @param $callback the function to call with the name of each file found.
51 * @param $fileregexp a regexp used to filter the search (optional).
b9c639d6 52 * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
3ef8c936 53 * only files that match the regexp will be included. (default false).
54 * @param array $ignorefolders will not go into any of these folders (optional).
b9c639d6 55 */
3ef8c936 56function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
57 $files = scandir($path);
58
59 foreach ($files as $file) {
60 $filepath = $path .'/'. $file;
2e710be8 61 if (strpos($file, '.') === 0) {
62 /// Don't check hidden files.
3ef8c936 63 continue;
64 } else if (is_dir($filepath)) {
65 if (!in_array($filepath, $ignorefolders)) {
66 recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
67 }
68 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
69 call_user_func($callback, $filepath);
70 }
71 }
72}
73
74/**
75 * An expectation for comparing strings ignoring whitespace.
b37eac91 76 *
77 * @package moodlecore
78 * @subpackage simpletestex
79 * @copyright &copy; 2006 The Open University
80 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3ef8c936 81 */
82class IgnoreWhitespaceExpectation extends SimpleExpectation {
83 var $expect;
84
85 function IgnoreWhitespaceExpectation($content, $message = '%s') {
86 $this->SimpleExpectation($message);
87 $this->expect=$this->normalise($content);
88 }
89
90 function test($ip) {
91 return $this->normalise($ip)==$this->expect;
92 }
93
94 function normalise($text) {
95 return preg_replace('/\s+/m',' ',trim($text));
96 }
97
98 function testMessage($ip) {
99 return "Input string [$ip] doesn't match the required value.";
100 }
101}
102
103/**
104 * An Expectation that two arrays contain the same list of values.
b37eac91 105 *
106 * @package moodlecore
107 * @subpackage simpletestex
108 * @copyright &copy; 2006 The Open University
109 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3ef8c936 110 */
111class ArraysHaveSameValuesExpectation extends SimpleExpectation {
112 var $expect;
113
114 function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
115 $this->SimpleExpectation($message);
116 if (!is_array($expected)) {
117 trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
118 'with an expected value that is not an array.');
119 }
120 $this->expect = $this->normalise($expected);
121 }
122
123 function test($actual) {
124 return $this->normalise($actual) == $this->expect;
125 }
126
127 function normalise($array) {
128 sort($array);
129 return $array;
130 }
131
132 function testMessage($actual) {
133 return 'Array [' . implode(', ', $actual) .
134 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
135 }
136}
137
8954245a 138
3ef8c936 139/**
140 * An Expectation that compares to objects, and ensures that for every field in the
141 * expected object, there is a key of the same name in the actual object, with
142 * the same value. (The actual object may have other fields to, but we ignore them.)
b37eac91 143 *
144 * @package moodlecore
145 * @subpackage simpletestex
146 * @copyright &copy; 2006 The Open University
147 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3ef8c936 148 */
149class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
150 var $expect;
151
152 function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
153 $this->SimpleExpectation($message);
154 if (!is_object($expected)) {
155 trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
156 'with an expected value that is not an object.');
157 }
158 $this->expect = $expected;
159 }
160
161 function test($actual) {
162 foreach ($this->expect as $key => $value) {
163 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
164 // OK
165 } else if (is_null($value) && is_null($actual->$key)) {
166 // OK
167 } else {
168 return false;
169 }
170 }
171 return true;
172 }
173
174 function testMessage($actual) {
175 $mismatches = array();
176 foreach ($this->expect as $key => $value) {
177 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
178 // OK
179 } else if (is_null($value) && is_null($actual->$key)) {
180 // OK
181 } else {
3efb762e 182 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
3ef8c936 183 }
184 }
185 return 'Actual object does not have all the same fields with the same values as the expected object (' .
186 implode(', ', $mismatches) . ').';
187 }
188}
189
d4bc6be7 190abstract class XMLStructureExpectation extends SimpleExpectation {
191 /**
192 * Parse a string as XML and return a DOMDocument;
193 * @param $html
194 * @return unknown_type
195 */
196 protected function load_xml($html) {
197 $prevsetting = libxml_use_internal_errors(true);
198 $parser = new DOMDocument();
91b652ac 199 if (!$parser->loadXML('<html>' . $html . '</html>')) {
d4bc6be7 200 $parser = new DOMDocument();
201 }
3d39d775 202 libxml_clear_errors();
203 libxml_use_internal_errors($prevsetting);
d4bc6be7 204 return $parser;
205 }
206}
8954245a 207/**
208 * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
209 *
210 * @copyright 2009 Tim Hunt
211 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
212 */
d4bc6be7 213class ContainsTagWithAttribute extends XMLStructureExpectation {
8954245a 214 protected $tag;
215 protected $attribute;
216 protected $value;
217
218 function __construct($tag, $attribute, $value, $message = '%s') {
d4bc6be7 219 parent::__construct($message);
8954245a 220 $this->tag = $tag;
221 $this->attribute = $attribute;
222 $this->value = $value;
223 }
224
225 function test($html) {
d4bc6be7 226 $parser = $this->load_xml($html);
6a923128 227 $list = $parser->getElementsByTagName($this->tag);
117bd748 228
6a923128 229 foreach ($list as $node) {
230 if ($node->attributes->getNamedItem($this->attribute)->nodeValue == $this->value) {
231 return true;
232 }
233 }
234 return false;
8954245a 235 }
236
237 function testMessage($html) {
238 return 'Content [' . $html . '] does not contain the tag [' .
239 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
240 }
241}
242
6a923128 243/**
244 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
245 * All attributes must be present and their values must match the expected values.
f8065dd2 246 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
6a923128 247 *
248 * @copyright 2009 Nicolas Connault
249 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
250 */
d4bc6be7 251class ContainsTagWithAttributes extends XMLStructureExpectation {
f8065dd2 252 /**
253 * @var string $tag The name of the Tag to search
254 */
6a923128 255 protected $tag;
f8065dd2 256 /**
257 * @var array $expectedvalues An associative array of parameters, all of which must be matched
258 */
259 protected $expectedvalues = array();
260 /**
261 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
262 */
263 protected $forbiddenvalues = array();
264 /**
265 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
266 */
267 protected $failurereason = 'nomatch';
268
269 function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
d4bc6be7 270 parent::__construct($message);
6a923128 271 $this->tag = $tag;
f8065dd2 272 $this->expectedvalues = $expectedvalues;
273 $this->forbiddenvalues = $forbiddenvalues;
6a923128 274 }
f8065dd2 275
6a923128 276 function test($html) {
d4bc6be7 277 $parser = $this->load_xml($html);
6a923128 278 $list = $parser->getElementsByTagName($this->tag);
f8065dd2 279
280 $foundamatch = false;
281
6a923128 282 // Iterating through inputs
283 foreach ($list as $node) {
284 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
285 continue;
286 }
287
f8065dd2 288 // For the current expected attribute under consideration, check that values match
289 $allattributesmatch = true;
6a923128 290
f8065dd2 291 foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
4c53f626 292 if (!$node->getAttribute($expectedattribute) && $expectedvalue != '') {
f8065dd2 293 $this->failurereason = 'nomatch';
294 continue 2; // Skip this tag, it doesn't have all the expected attributes
6a923128 295 }
f8065dd2 296 if ($node->getAttribute($expectedattribute) != $expectedvalue) {
297 $allattributesmatch = false;
298 $this->failurereason = 'nomatch';
6a923128 299 }
300 }
301
f8065dd2 302 if ($allattributesmatch) {
303 $foundamatch = true;
304
305 // Now make sure this node doesn't have any of the forbidden attributes either
306 $nodeattrlist = $node->attributes;
307
308 foreach ($nodeattrlist as $domattrname => $domattr) {
309 if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) == $this->forbiddenvalues[$domattrname]) {
310 $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
311 $foundamatch = false;
312 }
313 }
6a923128 314 }
6a923128 315 }
f8065dd2 316
317 return $foundamatch;
6a923128 318 }
f8065dd2 319
6a923128 320 function testMessage($html) {
f8065dd2 321 $output = 'Content [' . $html . '] ';
322
323 if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
324 $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
325 } else if ($this->failurereason == 'nomatch') {
326 $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
327 foreach ($this->expectedvalues as $var => $val) {
328 $output .= "$var=\"$val\" ";
329 }
330 $output = rtrim($output);
331 $output .= '].';
6a923128 332 }
f8065dd2 333
6a923128 334 return $output;
335 }
336}
8954245a 337
338/**
339 * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
340 *
341 * @copyright 2009 Tim Hunt
342 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
343 */
d4bc6be7 344class ContainsTagWithContents extends XMLStructureExpectation {
8954245a 345 protected $tag;
346 protected $content;
347
348 function __construct($tag, $content, $message = '%s') {
d4bc6be7 349 parent::__construct($message);
8954245a 350 $this->tag = $tag;
351 $this->content = $content;
352 }
353
354 function test($html) {
d4bc6be7 355 $parser = $this->load_xml($html);
6a923128 356 $list = $parser->getElementsByTagName($this->tag);
357
358 foreach ($list as $node) {
359 if ($node->textContent == $this->content) {
360 return true;
361 }
362 }
117bd748 363
6a923128 364 return false;
8954245a 365 }
366
367 function testMessage($html) {
368 return 'Content [' . $html . '] does not contain the tag [' .
369 $this->tag . '] with contents [' . $this->content . '].';
6a923128 370 }
371}
372
373/**
374 * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
375 *
376 * @copyright 2009 Nicolas Connault
377 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
378 */
d4bc6be7 379class ContainsEmptyTag extends XMLStructureExpectation {
6a923128 380 protected $tag;
381
382 function __construct($tag, $message = '%s') {
d4bc6be7 383 parent::__construct($message);
6a923128 384 $this->tag = $tag;
385 }
386
387 function test($html) {
d4bc6be7 388 $parser = $this->load_xml($html);
6a923128 389 $list = $parser->getElementsByTagName($this->tag);
390
391 foreach ($list as $node) {
392 if (!$node->hasAttributes() && !$node->hasChildNodes()) {
393 return true;
394 }
395 }
117bd748 396
6a923128 397 return false;
398 }
399
400 function testMessage($html) {
401 return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
8954245a 402 }
403}
404
405
f68cb08b 406/**
407 * This class lets you write unit tests that access a separate set of test
408 * tables with a different prefix. Only those tables you explicitly ask to
409 * be created will be.
240be1d7 410 *
411 * This class has failities for flipping $USER->id.
412 *
413 * The tear-down method for this class should automatically revert any changes
414 * you make during test set-up using the metods defined here. That is, it will
415 * drop tables for you automatically and revert to the real $DB and $USER->id.
b37eac91 416 *
417 * @package moodlecore
418 * @subpackage simpletestex
419 * @copyright &copy; 2006 The Open University
420 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
f68cb08b 421 */
422class UnitTestCaseUsingDatabase extends UnitTestCase {
423 private $realdb;
424 protected $testdb;
82701e24 425 private $realuserid = null;
f68cb08b 426 private $tables = array();
427
f68cb08b 428 public function __construct($label = false) {
429 global $DB, $CFG;
430
82701e24 431 // Complain if we get this far and $CFG->unittestprefix is not set.
f68cb08b 432 if (empty($CFG->unittestprefix)) {
433 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
434 }
82701e24 435
436 // Only do this after the above text.
f68cb08b 437 parent::UnitTestCase($label);
438
82701e24 439 // Create the test DB instance.
f68cb08b 440 $this->realdb = $DB;
441 $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
442 $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
443 }
444
445 /**
446 * Switch to using the test database for all queries until further notice.
f68cb08b 447 */
448 protected function switch_to_test_db() {
449 global $DB;
450 if ($DB === $this->testdb) {
451 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);
452 }
453 $DB = $this->testdb;
454 }
455
456 /**
457 * Revert to using the test database for all future queries.
458 */
459 protected function revert_to_real_db() {
460 global $DB;
461 if ($DB !== $this->testdb) {
7fa95ef1 462 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 463 }
464 $DB = $this->realdb;
465 }
466
82701e24 467 /**
468 * Switch $USER->id to a test value.
82701e24 469 *
470 * It might be worth making this method do more robuse $USER switching in future,
471 * however, this is sufficient for my needs at present.
472 */
473 protected function switch_global_user_id($userid) {
474 global $USER;
475 if (!is_null($this->realuserid)) {
476 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);
477 } else {
478 $this->realuserid = $USER->id;
479 }
480 $USER->id = $userid;
481 }
482
483 /**
484 * Revert $USER->id to the real value.
485 */
486 protected function revert_global_user_id() {
487 global $USER;
488 if (is_null($this->realuserid)) {
489 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);
490 } else {
491 $USER->id = $this->realuserid;
492 $this->realuserid = null;
493 }
494 }
495
f68cb08b 496 /**
497 * Check that the user has not forgotten to clean anything up, and if they
498 * have, display a rude message and clean it up for them.
499 */
240be1d7 500 private function automatic_clean_up() {
f68cb08b 501 global $DB;
82701e24 502 $cleanmore = false;
f68cb08b 503
240be1d7 504 // Drop any test tables that were created.
f68cb08b 505 foreach ($this->tables as $tablename => $notused) {
506 $this->drop_test_table($tablename);
507 }
508
240be1d7 509 // Switch back to the real DB if necessary.
f68cb08b 510 if ($DB !== $this->realdb) {
f68cb08b 511 $this->revert_to_real_db();
82701e24 512 $cleanmore = true;
513 }
514
240be1d7 515 // revert_global_user_id if necessary.
82701e24 516 if (!is_null($this->realuserid)) {
82701e24 517 $this->revert_global_user_id();
518 $cleanmore = true;
519 }
520
521 if ($cleanmore) {
522 accesslib_clear_all_caches_for_unit_testing();
f68cb08b 523 }
524 }
525
526 public function tearDown() {
240be1d7 527 $this->automatic_clean_up();
f68cb08b 528 parent::tearDown();
529 }
530
531 public function __destruct() {
240be1d7 532 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
533 $this->automatic_clean_up();
f68cb08b 534 }
535
536 /**
537 * Create a test table just like a real one, getting getting the definition from
538 * the specified install.xml file.
539 * @param string $tablename the name of the test table.
540 * @param string $installxmlfile the install.xml file in which this table is defined.
541 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
542 * so you need only specify, for example, 'mod/quiz'.
543 */
544 protected function create_test_table($tablename, $installxmlfile) {
545 global $CFG;
04aa2fd6 546 $dbman = $this->testdb->get_manager();
f68cb08b 547 if (isset($this->tables[$tablename])) {
c39a4607 548 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
f68cb08b 549 return;
550 }
04aa2fd6
EL
551 if ($dbman->table_exists($tablename)) {
552 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);
553 $dbman->drop_table(new xmldb_table($tablename));
554 }
c6adec25 555 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
f68cb08b 556 $this->tables[$tablename] = 1;
557 }
558
559 /**
560 * Convenience method for calling create_test_table repeatedly.
561 * @param array $tablenames an array of table names.
562 * @param string $installxmlfile the install.xml file in which this table is defined.
563 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
564 * so you need only specify, for example, 'mod/quiz'.
565 */
566 protected function create_test_tables($tablenames, $installxmlfile) {
567 foreach ($tablenames as $tablename) {
568 $this->create_test_table($tablename, $installxmlfile);
569 }
570 }
571
572 /**
573 * Drop a test table.
574 * @param $tablename the name of the test table.
575 */
576 protected function drop_test_table($tablename) {
577 if (!isset($this->tables[$tablename])) {
578 debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
579 return;
580 }
581 $dbman = $this->testdb->get_manager();
582 $table = new xmldb_table($tablename);
583 $dbman->drop_table($table);
584 unset($this->tables[$tablename]);
585 }
586
587 /**
588 * Convenience method for calling drop_test_table repeatedly.
589 * @param array $tablenames an array of table names.
590 */
591 protected function drop_test_tables($tablenames) {
592 foreach ($tablenames as $tablename) {
593 $this->drop_test_table($tablename);
594 }
595 }
596
597 /**
598 * Load a table with some rows of data. A typical call would look like:
599 *
600 * $config = $this->load_test_data('config_plugins',
601 * array('plugin', 'name', 'value'), array(
602 * array('frog', 'numlegs', 2),
603 * array('frog', 'sound', 'croak'),
604 * array('frog', 'action', 'jump'),
605 * ));
606 *
607 * @param string $table the table name.
608 * @param array $cols the columns to fill.
609 * @param array $data the data to load.
610 * @return array $objects corresponding to $data.
611 */
612 protected function load_test_data($table, array $cols, array $data) {
613 $results = array();
614 foreach ($data as $rowid => $row) {
615 $obj = new stdClass;
616 foreach ($cols as $key => $colname) {
617 $obj->$colname = $row[$key];
618 }
619 $obj->id = $this->testdb->insert_record($table, $obj);
620 $results[$rowid] = $obj;
621 }
622 return $results;
623 }
624
625 /**
626 * Clean up data loaded with load_test_data. The call corresponding to the
627 * example load above would be:
628 *
629 * $this->delete_test_data('config_plugins', $config);
630 *
631 * @param string $table the table name.
632 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
633 */
634 protected function delete_test_data($table, array $rows) {
635 $ids = array();
636 foreach ($rows as $row) {
637 $ids[] = $row->id;
638 }
639 $this->testdb->delete_records_list($table, 'id', $ids);
640 }
641}
642
8954245a 643
b37eac91 644/**
645 * @package moodlecore
646 * @subpackage simpletestex
647 * @copyright &copy; 2006 The Open University
648 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
649 */
58fa5d6f 650class FakeDBUnitTestCase extends UnitTestCase {
356e0010 651 public $tables = array();
90997d6d 652 public $pkfile;
653 public $cfg;
274e2947 654 public $DB;
356e0010 655
90997d6d 656 /**
657 * In the constructor, record the max(id) of each test table into a csv file.
658 * If this file already exists, it means that a previous run of unit tests
659 * did not complete, and has left data undeleted in the DB. This data is then
660 * deleted and the file is retained. Otherwise it is created.
b37eac91 661 *
662 * throws moodle_exception if CSV file cannot be created
90997d6d 663 */
b9c639d6 664 public function __construct($label = false) {
84ebf08d 665 global $DB, $CFG;
666
667 if (empty($CFG->unittestprefix)) {
668 return;
669 }
670
b9c639d6 671 parent::UnitTestCase($label);
90997d6d 672 // MDL-16483 Get PKs and save data to text file
84ebf08d 673
90997d6d 674 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
675 $this->cfg = $CFG;
274e2947 676
677 UnitTestDB::instantiate();
90997d6d 678
679 $tables = $DB->get_tables();
680
681 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
682 if (file_exists($this->pkfile)) {
683 $this->truncate_test_tables($this->get_table_data($this->pkfile));
684
685 } else { // Create the file
686 $tabledata = '';
687
688 foreach ($tables as $table) {
58fa5d6f 689 if ($table != 'sessions') {
90997d6d 690 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
691 $max_id = 0;
692 }
693 $tabledata .= "$table, $max_id\n";
694 }
695 }
696 if (!file_put_contents($this->pkfile, $tabledata)) {
5d1381c2 697 $a = new stdClass();
698 $a->filename = $this->pkfile;
699 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
90997d6d 700 }
701 }
702 }
703
704 /**
705 * 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.
706 * @param array $tabledata
707 */
708 private function truncate_test_tables($tabledata) {
709 global $CFG, $DB;
710
84ebf08d 711 if (empty($CFG->unittestprefix)) {
712 return;
713 }
714
90997d6d 715 $tables = $DB->get_tables();
716
717 foreach ($tables as $table) {
3f57bd45 718 if ($table != 'sessions' && isset($tabledata[$table])) {
5d1381c2 719 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
90997d6d 720 }
721 }
356e0010 722 }
723
90997d6d 724 /**
725 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
726 * 1. Table name
727 * 2. Max id
b37eac91 728 *
729 * throws moodle_exception if file doesn't exist
730 *
90997d6d 731 * @param string $filename
90997d6d 732 */
733 public function get_table_data($filename) {
84ebf08d 734 global $CFG;
735
736 if (empty($CFG->unittestprefix)) {
737 return;
738 }
739
90997d6d 740 if (file_exists($this->pkfile)) {
741 $handle = fopen($this->pkfile, 'r');
742 $tabledata = array();
743
744 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
745 $tabledata[$data[0]] = $data[1];
746 }
747 return $tabledata;
748 } else {
5d1381c2 749 $a = new stdClass();
750 $a->filename = $this->pkfile;
5d1381c2 751 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
90997d6d 752 return false;
753 }
754 }
755
756 /**
757 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
758 * Also detects if this config setting is properly set, and if the user table exists.
b37eac91 759 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
90997d6d 760 */
356e0010 761 public function setUp() {
84ebf08d 762 global $DB, $CFG;
763
764 if (empty($CFG->unittestprefix)) {
765 return;
766 }
767
356e0010 768 parent::setUp();
274e2947 769 $this->DB =& $DB;
915d745f 770 ob_start();
5d1381c2 771 }
772
773 /**
774 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
775 */
776 public function tearDown() {
84ebf08d 777 global $DB, $CFG;
778
779 if (empty($CFG->unittestprefix)) {
780 return;
781 }
782
274e2947 783 if (empty($DB)) {
784 $DB = $this->DB;
785 }
5d1381c2 786 $DB->cleanup();
787 parent::tearDown();
747f4a4c 788
789 // Output buffering
915d745f 790 if (ob_get_length() > 0) {
791 ob_end_flush();
792 }
5d1381c2 793 }
794
795 /**
796 * 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
797 * It should also detect if data is missing from the original tables.
798 */
799 public function __destruct() {
800 global $CFG, $DB;
801
84ebf08d 802 if (empty($CFG->unittestprefix)) {
803 return;
804 }
805
5d1381c2 806 $CFG = $this->cfg;
807 $this->tearDown();
808 UnitTestDB::restore();
809 fulldelete($this->pkfile);
810 }
821e4ecf 811
812 /**
813 * Load a table with some rows of data. A typical call would look like:
814 *
815 * $config = $this->load_test_data('config_plugins',
816 * array('plugin', 'name', 'value'), array(
817 * array('frog', 'numlegs', 2),
818 * array('frog', 'sound', 'croak'),
819 * array('frog', 'action', 'jump'),
820 * ));
821 *
822 * @param string $table the table name.
823 * @param array $cols the columns to fill.
824 * @param array $data the data to load.
825 * @return array $objects corresponding to $data.
826 */
827 public function load_test_data($table, array $cols, array $data) {
84ebf08d 828 global $CFG, $DB;
829
830 if (empty($CFG->unittestprefix)) {
831 return;
832 }
833
821e4ecf 834 $results = array();
835 foreach ($data as $rowid => $row) {
836 $obj = new stdClass;
837 foreach ($cols as $key => $colname) {
838 $obj->$colname = $row[$key];
839 }
840 $obj->id = $DB->insert_record($table, $obj);
841 $results[$rowid] = $obj;
842 }
843 return $results;
844 }
845
846 /**
847 * Clean up data loaded with load_test_data. The call corresponding to the
848 * example load above would be:
849 *
850 * $this->delete_test_data('config_plugins', $config);
851 *
852 * @param string $table the table name.
853 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
854 */
855 public function delete_test_data($table, array $rows) {
84ebf08d 856 global $CFG, $DB;
857
858 if (empty($CFG->unittestprefix)) {
859 return;
860 }
861
821e4ecf 862 $ids = array();
863 foreach ($rows as $row) {
864 $ids[] = $row->id;
865 }
866 $DB->delete_records_list($table, 'id', $ids);
867 }
5d1381c2 868}
869
870/**
871 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
872 * static instantiate() method, and restores the original global $DB through restore().
873 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
874 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
875 * without subclassing it.
b37eac91 876 *
877 * @package moodlecore
878 * @subpackage simpletestex
879 * @copyright &copy; 2006 The Open University
880 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5d1381c2 881 */
882class UnitTestDB {
883 public static $DB;
884 private static $real_db;
356e0010 885
5d1381c2 886 public $table_data = array();
887
5d1381c2 888 /**
889 * Call this statically to connect to the DB using the unittest prefix, instantiate
890 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
891 */
892 public static function instantiate() {
893 global $CFG, $DB;
894 UnitTestDB::$real_db = clone($DB);
90997d6d 895 if (empty($CFG->unittestprefix)) {
b9c639d6 896 print_error("prefixnotset", 'simpletest');
897 }
898
274e2947 899 if (empty(UnitTestDB::$DB)) {
900 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
beaa43db 901 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
274e2947 902 }
903
5d1381c2 904 $manager = UnitTestDB::$DB->get_manager();
356e0010 905
906 if (!$manager->table_exists('user')) {
b9c639d6 907 print_error('tablesnotsetup', 'simpletest');
908 }
5d1381c2 909
910 $DB = new UnitTestDB();
911 }
912
913 public function __call($method, $args) {
914 // Set args to null if they don't exist (up to 10 args should do)
915 if (!method_exists($this, $method)) {
916 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
917 } else {
918 call_user_func_array(array($this, $method), $args);
919 }
920 }
921
922 public function __get($variable) {
923 return UnitTestDB::$DB->$variable;
924 }
925
926 public function __set($variable, $value) {
927 UnitTestDB::$DB->$variable = $value;
928 }
929
930 public function __isset($variable) {
931 return isset(UnitTestDB::$DB->$variable);
932 }
933
934 public function __unset($variable) {
935 unset(UnitTestDB::$DB->$variable);
b9c639d6 936 }
937
90997d6d 938 /**
5d1381c2 939 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
90997d6d 940 */
5d1381c2 941 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
356e0010 942 global $DB;
5d1381c2 943 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
944 $this->table_data[$table][] = $id;
945 return $id;
946 }
356e0010 947
5d1381c2 948 /**
949 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
950 * throw an exception and cancel update.
117bd748 951 *
b37eac91 952 * throws moodle_exception If trying to update a record not inserted by unit tests.
5d1381c2 953 */
954 public function update_record($table, $dataobject, $bulk=false) {
955 global $DB;
747f4a4c 956 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
274e2947 957 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
958 $a = new stdClass();
959 $a->id = $dataobject->id;
960 $a->table = $table;
274e2947 961 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
5d1381c2 962 } else {
963 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
964 }
b9c639d6 965 }
966
967 /**
5d1381c2 968 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
969 * throw an exception and cancel delete.
b37eac91 970 *
971 * throws moodle_exception If trying to delete a record not inserted by unit tests.
b9c639d6 972 */
274e2947 973 public function delete_records($table, array $conditions=array()) {
5d1381c2 974 global $DB;
747f4a4c 975 $tables_to_ignore = array('context_temp');
976
5d1381c2 977 $a = new stdClass();
978 $a->table = $table;
979
980 // Get ids matching conditions
981 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
982 return UnitTestDB::$DB->delete_records($table, $conditions);
983 }
984
985 $proceed_with_delete = true;
986
987 if (!is_array($ids_to_delete)) {
988 $ids_to_delete = array($ids_to_delete);
989 }
990
991 foreach ($ids_to_delete as $id) {
747f4a4c 992 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
5d1381c2 993 $proceed_with_delete = false;
994 $a->id = $id;
995 break;
996 }
997 }
998
999 if ($proceed_with_delete) {
1000 return UnitTestDB::$DB->delete_records($table, $conditions);
1001 } else {
5d1381c2 1002 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1003 }
b9c639d6 1004 }
b9c639d6 1005
5d1381c2 1006 /**
1007 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1008 * throw an exception and cancel delete.
b37eac91 1009 *
1010 * throws moodle_exception If trying to delete a record not inserted by unit tests.
5d1381c2 1011 */
1012 public function delete_records_select($table, $select, array $params=null) {
1013 global $DB;
1014 $a = new stdClass();
1015 $a->table = $table;
1016
1017 // Get ids matching conditions
1018 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1019 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1020 }
1021
1022 $proceed_with_delete = true;
1023
1024 foreach ($ids_to_delete as $id) {
1025 if (!in_array($id, $this->table_data[$table])) {
1026 $proceed_with_delete = false;
1027 $a->id = $id;
1028 break;
1029 }
1030 }
1031
1032 if ($proceed_with_delete) {
1033 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1034 } else {
5d1381c2 1035 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
1036 }
1037 }
1038
1039 /**
1040 * Removes from the test DB all the records that were inserted during unit tests,
1041 */
1042 public function cleanup() {
1043 global $DB;
1044 foreach ($this->table_data as $table => $ids) {
1045 foreach ($ids as $id) {
1046 $DB->delete_records($table, array('id' => $id));
1047 }
1048 }
1049 }
1050
1051 /**
1052 * Restores the global $DB object.
1053 */
1054 public static function restore() {
1055 global $DB;
1056 $DB = UnitTestDB::$real_db;
1057 }
1058
1059 public function get_field($table, $return, array $conditions) {
1060 if (!is_array($conditions)) {
fa98e6d1 1061 throw new coding_exception('$conditions is not an array.');
5d1381c2 1062 }
1063 return UnitTestDB::$DB->get_field($table, $return, $conditions);
1064 }
1065}