Unit tests: show total time taken to run the tests.
[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
138/**
139 * An Expectation that compares to objects, and ensures that for every field in the
140 * expected object, there is a key of the same name in the actual object, with
141 * the same value. (The actual object may have other fields to, but we ignore them.)
b37eac91 142 *
143 * @package moodlecore
144 * @subpackage simpletestex
145 * @copyright &copy; 2006 The Open University
146 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3ef8c936 147 */
148class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
149 var $expect;
150
151 function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
152 $this->SimpleExpectation($message);
153 if (!is_object($expected)) {
154 trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
155 'with an expected value that is not an object.');
156 }
157 $this->expect = $expected;
158 }
159
160 function test($actual) {
161 foreach ($this->expect as $key => $value) {
162 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
163 // OK
164 } else if (is_null($value) && is_null($actual->$key)) {
165 // OK
166 } else {
167 return false;
168 }
169 }
170 return true;
171 }
172
173 function testMessage($actual) {
174 $mismatches = array();
175 foreach ($this->expect as $key => $value) {
176 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
177 // OK
178 } else if (is_null($value) && is_null($actual->$key)) {
179 // OK
180 } else {
3efb762e 181 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
3ef8c936 182 }
183 }
184 return 'Actual object does not have all the same fields with the same values as the expected object (' .
185 implode(', ', $mismatches) . ').';
186 }
187}
188
f68cb08b 189/**
190 * This class lets you write unit tests that access a separate set of test
191 * tables with a different prefix. Only those tables you explicitly ask to
192 * be created will be.
240be1d7 193 *
194 * This class has failities for flipping $USER->id.
195 *
196 * The tear-down method for this class should automatically revert any changes
197 * you make during test set-up using the metods defined here. That is, it will
198 * drop tables for you automatically and revert to the real $DB and $USER->id.
b37eac91 199 *
200 * @package moodlecore
201 * @subpackage simpletestex
202 * @copyright &copy; 2006 The Open University
203 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
f68cb08b 204 */
205class UnitTestCaseUsingDatabase extends UnitTestCase {
206 private $realdb;
207 protected $testdb;
82701e24 208 private $realuserid = null;
f68cb08b 209 private $tables = array();
210
f68cb08b 211 public function __construct($label = false) {
212 global $DB, $CFG;
213
82701e24 214 // Complain if we get this far and $CFG->unittestprefix is not set.
f68cb08b 215 if (empty($CFG->unittestprefix)) {
216 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
217 }
82701e24 218
219 // Only do this after the above text.
f68cb08b 220 parent::UnitTestCase($label);
221
82701e24 222 // Create the test DB instance.
f68cb08b 223 $this->realdb = $DB;
224 $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
225 $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
226 }
227
228 /**
229 * Switch to using the test database for all queries until further notice.
f68cb08b 230 */
231 protected function switch_to_test_db() {
232 global $DB;
233 if ($DB === $this->testdb) {
234 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);
235 }
236 $DB = $this->testdb;
237 }
238
239 /**
240 * Revert to using the test database for all future queries.
241 */
242 protected function revert_to_real_db() {
243 global $DB;
244 if ($DB !== $this->testdb) {
7fa95ef1 245 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 246 }
247 $DB = $this->realdb;
248 }
249
82701e24 250 /**
251 * Switch $USER->id to a test value.
82701e24 252 *
253 * It might be worth making this method do more robuse $USER switching in future,
254 * however, this is sufficient for my needs at present.
255 */
256 protected function switch_global_user_id($userid) {
257 global $USER;
258 if (!is_null($this->realuserid)) {
259 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);
260 } else {
261 $this->realuserid = $USER->id;
262 }
263 $USER->id = $userid;
264 }
265
266 /**
267 * Revert $USER->id to the real value.
268 */
269 protected function revert_global_user_id() {
270 global $USER;
271 if (is_null($this->realuserid)) {
272 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);
273 } else {
274 $USER->id = $this->realuserid;
275 $this->realuserid = null;
276 }
277 }
278
f68cb08b 279 /**
280 * Check that the user has not forgotten to clean anything up, and if they
281 * have, display a rude message and clean it up for them.
282 */
240be1d7 283 private function automatic_clean_up() {
f68cb08b 284 global $DB;
82701e24 285 $cleanmore = false;
f68cb08b 286
240be1d7 287 // Drop any test tables that were created.
f68cb08b 288 foreach ($this->tables as $tablename => $notused) {
289 $this->drop_test_table($tablename);
290 }
291
240be1d7 292 // Switch back to the real DB if necessary.
f68cb08b 293 if ($DB !== $this->realdb) {
f68cb08b 294 $this->revert_to_real_db();
82701e24 295 $cleanmore = true;
296 }
297
240be1d7 298 // revert_global_user_id if necessary.
82701e24 299 if (!is_null($this->realuserid)) {
82701e24 300 $this->revert_global_user_id();
301 $cleanmore = true;
302 }
303
304 if ($cleanmore) {
305 accesslib_clear_all_caches_for_unit_testing();
f68cb08b 306 }
307 }
308
309 public function tearDown() {
240be1d7 310 $this->automatic_clean_up();
f68cb08b 311 parent::tearDown();
312 }
313
314 public function __destruct() {
240be1d7 315 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
316 $this->automatic_clean_up();
f68cb08b 317 }
318
319 /**
320 * Create a test table just like a real one, getting getting the definition from
321 * the specified install.xml file.
322 * @param string $tablename the name of the test table.
323 * @param string $installxmlfile the install.xml file in which this table is defined.
324 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
325 * so you need only specify, for example, 'mod/quiz'.
326 */
327 protected function create_test_table($tablename, $installxmlfile) {
328 global $CFG;
329 if (isset($this->tables[$tablename])) {
c39a4607 330 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
f68cb08b 331 return;
332 }
333 $dbman = $this->testdb->get_manager();
334 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename);
335 $this->tables[$tablename] = 1;
336 }
337
338 /**
339 * Convenience method for calling create_test_table repeatedly.
340 * @param array $tablenames an array of table names.
341 * @param string $installxmlfile the install.xml file in which this table is defined.
342 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
343 * so you need only specify, for example, 'mod/quiz'.
344 */
345 protected function create_test_tables($tablenames, $installxmlfile) {
346 foreach ($tablenames as $tablename) {
347 $this->create_test_table($tablename, $installxmlfile);
348 }
349 }
350
351 /**
352 * Drop a test table.
353 * @param $tablename the name of the test table.
354 */
355 protected function drop_test_table($tablename) {
356 if (!isset($this->tables[$tablename])) {
357 debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
358 return;
359 }
360 $dbman = $this->testdb->get_manager();
361 $table = new xmldb_table($tablename);
362 $dbman->drop_table($table);
363 unset($this->tables[$tablename]);
364 }
365
366 /**
367 * Convenience method for calling drop_test_table repeatedly.
368 * @param array $tablenames an array of table names.
369 */
370 protected function drop_test_tables($tablenames) {
371 foreach ($tablenames as $tablename) {
372 $this->drop_test_table($tablename);
373 }
374 }
375
376 /**
377 * Load a table with some rows of data. A typical call would look like:
378 *
379 * $config = $this->load_test_data('config_plugins',
380 * array('plugin', 'name', 'value'), array(
381 * array('frog', 'numlegs', 2),
382 * array('frog', 'sound', 'croak'),
383 * array('frog', 'action', 'jump'),
384 * ));
385 *
386 * @param string $table the table name.
387 * @param array $cols the columns to fill.
388 * @param array $data the data to load.
389 * @return array $objects corresponding to $data.
390 */
391 protected function load_test_data($table, array $cols, array $data) {
392 $results = array();
393 foreach ($data as $rowid => $row) {
394 $obj = new stdClass;
395 foreach ($cols as $key => $colname) {
396 $obj->$colname = $row[$key];
397 }
398 $obj->id = $this->testdb->insert_record($table, $obj);
399 $results[$rowid] = $obj;
400 }
401 return $results;
402 }
403
404 /**
405 * Clean up data loaded with load_test_data. The call corresponding to the
406 * example load above would be:
407 *
408 * $this->delete_test_data('config_plugins', $config);
409 *
410 * @param string $table the table name.
411 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
412 */
413 protected function delete_test_data($table, array $rows) {
414 $ids = array();
415 foreach ($rows as $row) {
416 $ids[] = $row->id;
417 }
418 $this->testdb->delete_records_list($table, 'id', $ids);
419 }
420}
421
b37eac91 422/**
423 * @package moodlecore
424 * @subpackage simpletestex
425 * @copyright &copy; 2006 The Open University
426 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
427 */
58fa5d6f 428class FakeDBUnitTestCase extends UnitTestCase {
356e0010 429 public $tables = array();
90997d6d 430 public $pkfile;
431 public $cfg;
274e2947 432 public $DB;
356e0010 433
90997d6d 434 /**
435 * In the constructor, record the max(id) of each test table into a csv file.
436 * If this file already exists, it means that a previous run of unit tests
437 * did not complete, and has left data undeleted in the DB. This data is then
438 * deleted and the file is retained. Otherwise it is created.
b37eac91 439 *
440 * throws moodle_exception if CSV file cannot be created
90997d6d 441 */
b9c639d6 442 public function __construct($label = false) {
84ebf08d 443 global $DB, $CFG;
444
445 if (empty($CFG->unittestprefix)) {
446 return;
447 }
448
b9c639d6 449 parent::UnitTestCase($label);
90997d6d 450 // MDL-16483 Get PKs and save data to text file
84ebf08d 451
90997d6d 452 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
453 $this->cfg = $CFG;
274e2947 454
455 UnitTestDB::instantiate();
90997d6d 456
457 $tables = $DB->get_tables();
458
459 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
460 if (file_exists($this->pkfile)) {
461 $this->truncate_test_tables($this->get_table_data($this->pkfile));
462
463 } else { // Create the file
464 $tabledata = '';
465
466 foreach ($tables as $table) {
58fa5d6f 467 if ($table != 'sessions') {
90997d6d 468 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
469 $max_id = 0;
470 }
471 $tabledata .= "$table, $max_id\n";
472 }
473 }
474 if (!file_put_contents($this->pkfile, $tabledata)) {
5d1381c2 475 $a = new stdClass();
476 $a->filename = $this->pkfile;
477 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
90997d6d 478 }
479 }
480 }
481
482 /**
483 * 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.
484 * @param array $tabledata
485 */
486 private function truncate_test_tables($tabledata) {
487 global $CFG, $DB;
488
84ebf08d 489 if (empty($CFG->unittestprefix)) {
490 return;
491 }
492
90997d6d 493 $tables = $DB->get_tables();
494
495 foreach ($tables as $table) {
3f57bd45 496 if ($table != 'sessions' && isset($tabledata[$table])) {
5d1381c2 497 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
90997d6d 498 }
499 }
356e0010 500 }
501
90997d6d 502 /**
503 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
504 * 1. Table name
505 * 2. Max id
b37eac91 506 *
507 * throws moodle_exception if file doesn't exist
508 *
90997d6d 509 * @param string $filename
90997d6d 510 */
511 public function get_table_data($filename) {
84ebf08d 512 global $CFG;
513
514 if (empty($CFG->unittestprefix)) {
515 return;
516 }
517
90997d6d 518 if (file_exists($this->pkfile)) {
519 $handle = fopen($this->pkfile, 'r');
520 $tabledata = array();
521
522 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
523 $tabledata[$data[0]] = $data[1];
524 }
525 return $tabledata;
526 } else {
5d1381c2 527 $a = new stdClass();
528 $a->filename = $this->pkfile;
529 debug_print_backtrace();
530 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
90997d6d 531 return false;
532 }
533 }
534
535 /**
536 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
537 * Also detects if this config setting is properly set, and if the user table exists.
b37eac91 538 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
90997d6d 539 */
356e0010 540 public function setUp() {
84ebf08d 541 global $DB, $CFG;
542
543 if (empty($CFG->unittestprefix)) {
544 return;
545 }
546
356e0010 547 parent::setUp();
274e2947 548 $this->DB =& $DB;
915d745f 549 ob_start();
5d1381c2 550 }
551
552 /**
553 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
554 */
555 public function tearDown() {
84ebf08d 556 global $DB, $CFG;
557
558 if (empty($CFG->unittestprefix)) {
559 return;
560 }
561
274e2947 562 if (empty($DB)) {
563 $DB = $this->DB;
564 }
5d1381c2 565 $DB->cleanup();
566 parent::tearDown();
747f4a4c 567
568 // Output buffering
915d745f 569 if (ob_get_length() > 0) {
570 ob_end_flush();
571 }
5d1381c2 572 }
573
574 /**
575 * 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
576 * It should also detect if data is missing from the original tables.
577 */
578 public function __destruct() {
579 global $CFG, $DB;
580
84ebf08d 581 if (empty($CFG->unittestprefix)) {
582 return;
583 }
584
5d1381c2 585 $CFG = $this->cfg;
586 $this->tearDown();
587 UnitTestDB::restore();
588 fulldelete($this->pkfile);
589 }
821e4ecf 590
591 /**
592 * Load a table with some rows of data. A typical call would look like:
593 *
594 * $config = $this->load_test_data('config_plugins',
595 * array('plugin', 'name', 'value'), array(
596 * array('frog', 'numlegs', 2),
597 * array('frog', 'sound', 'croak'),
598 * array('frog', 'action', 'jump'),
599 * ));
600 *
601 * @param string $table the table name.
602 * @param array $cols the columns to fill.
603 * @param array $data the data to load.
604 * @return array $objects corresponding to $data.
605 */
606 public function load_test_data($table, array $cols, array $data) {
84ebf08d 607 global $CFG, $DB;
608
609 if (empty($CFG->unittestprefix)) {
610 return;
611 }
612
821e4ecf 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 = $DB->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 public function delete_test_data($table, array $rows) {
84ebf08d 635 global $CFG, $DB;
636
637 if (empty($CFG->unittestprefix)) {
638 return;
639 }
640
821e4ecf 641 $ids = array();
642 foreach ($rows as $row) {
643 $ids[] = $row->id;
644 }
645 $DB->delete_records_list($table, 'id', $ids);
646 }
5d1381c2 647}
648
649/**
650 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
651 * static instantiate() method, and restores the original global $DB through restore().
652 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
653 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
654 * without subclassing it.
b37eac91 655 *
656 * @package moodlecore
657 * @subpackage simpletestex
658 * @copyright &copy; 2006 The Open University
659 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5d1381c2 660 */
661class UnitTestDB {
662 public static $DB;
663 private static $real_db;
356e0010 664
5d1381c2 665 public $table_data = array();
666
5d1381c2 667 /**
668 * Call this statically to connect to the DB using the unittest prefix, instantiate
669 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
670 */
671 public static function instantiate() {
672 global $CFG, $DB;
673 UnitTestDB::$real_db = clone($DB);
90997d6d 674 if (empty($CFG->unittestprefix)) {
b9c639d6 675 print_error("prefixnotset", 'simpletest');
676 }
677
274e2947 678 if (empty(UnitTestDB::$DB)) {
679 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
beaa43db 680 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
274e2947 681 }
682
5d1381c2 683 $manager = UnitTestDB::$DB->get_manager();
356e0010 684
685 if (!$manager->table_exists('user')) {
b9c639d6 686 print_error('tablesnotsetup', 'simpletest');
687 }
5d1381c2 688
689 $DB = new UnitTestDB();
690 }
691
692 public function __call($method, $args) {
693 // Set args to null if they don't exist (up to 10 args should do)
694 if (!method_exists($this, $method)) {
695 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
696 } else {
697 call_user_func_array(array($this, $method), $args);
698 }
699 }
700
701 public function __get($variable) {
702 return UnitTestDB::$DB->$variable;
703 }
704
705 public function __set($variable, $value) {
706 UnitTestDB::$DB->$variable = $value;
707 }
708
709 public function __isset($variable) {
710 return isset(UnitTestDB::$DB->$variable);
711 }
712
713 public function __unset($variable) {
714 unset(UnitTestDB::$DB->$variable);
b9c639d6 715 }
716
90997d6d 717 /**
5d1381c2 718 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
90997d6d 719 */
5d1381c2 720 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
356e0010 721 global $DB;
5d1381c2 722 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
723 $this->table_data[$table][] = $id;
724 return $id;
725 }
356e0010 726
5d1381c2 727 /**
728 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
729 * throw an exception and cancel update.
b37eac91 730 *
731 * throws moodle_exception If trying to update a record not inserted by unit tests.
5d1381c2 732 */
733 public function update_record($table, $dataobject, $bulk=false) {
734 global $DB;
747f4a4c 735 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
274e2947 736 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
737 $a = new stdClass();
738 $a->id = $dataobject->id;
739 $a->table = $table;
740 debug_print_backtrace();
741 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
5d1381c2 742 } else {
743 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
744 }
b9c639d6 745 }
746
747 /**
5d1381c2 748 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
749 * throw an exception and cancel delete.
b37eac91 750 *
751 * throws moodle_exception If trying to delete a record not inserted by unit tests.
b9c639d6 752 */
274e2947 753 public function delete_records($table, array $conditions=array()) {
5d1381c2 754 global $DB;
747f4a4c 755 $tables_to_ignore = array('context_temp');
756
5d1381c2 757 $a = new stdClass();
758 $a->table = $table;
759
760 // Get ids matching conditions
761 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
762 return UnitTestDB::$DB->delete_records($table, $conditions);
763 }
764
765 $proceed_with_delete = true;
766
767 if (!is_array($ids_to_delete)) {
768 $ids_to_delete = array($ids_to_delete);
769 }
770
771 foreach ($ids_to_delete as $id) {
747f4a4c 772 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
5d1381c2 773 $proceed_with_delete = false;
774 $a->id = $id;
775 break;
776 }
777 }
778
779 if ($proceed_with_delete) {
780 return UnitTestDB::$DB->delete_records($table, $conditions);
781 } else {
782 debug_print_backtrace();
783 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
784 }
b9c639d6 785 }
b9c639d6 786
5d1381c2 787 /**
788 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
789 * throw an exception and cancel delete.
b37eac91 790 *
791 * throws moodle_exception If trying to delete a record not inserted by unit tests.
5d1381c2 792 */
793 public function delete_records_select($table, $select, array $params=null) {
794 global $DB;
795 $a = new stdClass();
796 $a->table = $table;
797
798 // Get ids matching conditions
799 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
800 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
801 }
802
803 $proceed_with_delete = true;
804
805 foreach ($ids_to_delete as $id) {
806 if (!in_array($id, $this->table_data[$table])) {
807 $proceed_with_delete = false;
808 $a->id = $id;
809 break;
810 }
811 }
812
813 if ($proceed_with_delete) {
814 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
815 } else {
816 debug_print_backtrace();
817 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
818 }
819 }
820
821 /**
822 * Removes from the test DB all the records that were inserted during unit tests,
823 */
824 public function cleanup() {
825 global $DB;
826 foreach ($this->table_data as $table => $ids) {
827 foreach ($ids as $id) {
828 $DB->delete_records($table, array('id' => $id));
829 }
830 }
831 }
832
833 /**
834 * Restores the global $DB object.
835 */
836 public static function restore() {
837 global $DB;
838 $DB = UnitTestDB::$real_db;
839 }
840
841 public function get_field($table, $return, array $conditions) {
842 if (!is_array($conditions)) {
843 debug_print_backtrace();
844 }
845 return UnitTestDB::$DB->get_field($table, $return, $conditions);
846 }
847}
bb48a537 848?>