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