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