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