ddl: New method install_one_table_from_xmldb_file ready for MDL-18607 unit test changes.
[moodle.git] / lib / simpletestlib.php
CommitLineData
56768525 1<?php // $Id$
3ef8c936 2/**
3 * Utility functions to make unit testing easier.
b9c639d6 4 *
3ef8c936 5 * These functions, particularly the the database ones, are quick and
b9c639d6 6 * dirty methods for getting things done in test cases. None of these
3ef8c936 7 * methods should be used outside test code.
8 *
9 * @copyright &copy; 2006 The Open University
10 * @author T.J.Hunt@open.ac.uk
11 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
12 * @version $Id$
13 * @package SimpleTestEx
14 */
15
16require_once(dirname(__FILE__) . '/../config.php');
17require_once($CFG->libdir . '/simpletestlib/simpletest.php');
18require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
19require_once($CFG->libdir . '/simpletestlib/expectation.php');
4309bd17 20require_once($CFG->libdir . '/simpletestlib/reporter.php');
a205dcdc 21require_once($CFG->libdir . '/simpletestlib/web_tester.php');
76144765 22require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
3ef8c936 23
24/**
25 * Recursively visit all the files in the source tree. Calls the callback
b9c639d6 26 * function with the pathname of each file found.
27 *
28 * @param $path the folder to start searching from.
3ef8c936 29 * @param $callback the function to call with the name of each file found.
30 * @param $fileregexp a regexp used to filter the search (optional).
b9c639d6 31 * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
3ef8c936 32 * only files that match the regexp will be included. (default false).
33 * @param array $ignorefolders will not go into any of these folders (optional).
b9c639d6 34 */
3ef8c936 35function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
36 $files = scandir($path);
37
38 foreach ($files as $file) {
39 $filepath = $path .'/'. $file;
2e710be8 40 if (strpos($file, '.') === 0) {
41 /// Don't check hidden files.
3ef8c936 42 continue;
43 } else if (is_dir($filepath)) {
44 if (!in_array($filepath, $ignorefolders)) {
45 recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
46 }
47 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
48 call_user_func($callback, $filepath);
49 }
50 }
51}
52
53/**
54 * An expectation for comparing strings ignoring whitespace.
55 */
56class IgnoreWhitespaceExpectation extends SimpleExpectation {
57 var $expect;
58
59 function IgnoreWhitespaceExpectation($content, $message = '%s') {
60 $this->SimpleExpectation($message);
61 $this->expect=$this->normalise($content);
62 }
63
64 function test($ip) {
65 return $this->normalise($ip)==$this->expect;
66 }
67
68 function normalise($text) {
69 return preg_replace('/\s+/m',' ',trim($text));
70 }
71
72 function testMessage($ip) {
73 return "Input string [$ip] doesn't match the required value.";
74 }
75}
76
77/**
78 * An Expectation that two arrays contain the same list of values.
79 */
80class ArraysHaveSameValuesExpectation extends SimpleExpectation {
81 var $expect;
82
83 function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
84 $this->SimpleExpectation($message);
85 if (!is_array($expected)) {
86 trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
87 'with an expected value that is not an array.');
88 }
89 $this->expect = $this->normalise($expected);
90 }
91
92 function test($actual) {
93 return $this->normalise($actual) == $this->expect;
94 }
95
96 function normalise($array) {
97 sort($array);
98 return $array;
99 }
100
101 function testMessage($actual) {
102 return 'Array [' . implode(', ', $actual) .
103 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
104 }
105}
106
107/**
108 * An Expectation that compares to objects, and ensures that for every field in the
109 * expected object, there is a key of the same name in the actual object, with
110 * the same value. (The actual object may have other fields to, but we ignore them.)
111 */
112class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
113 var $expect;
114
115 function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
116 $this->SimpleExpectation($message);
117 if (!is_object($expected)) {
118 trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
119 'with an expected value that is not an object.');
120 }
121 $this->expect = $expected;
122 }
123
124 function test($actual) {
125 foreach ($this->expect as $key => $value) {
126 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
127 // OK
128 } else if (is_null($value) && is_null($actual->$key)) {
129 // OK
130 } else {
131 return false;
132 }
133 }
134 return true;
135 }
136
137 function testMessage($actual) {
138 $mismatches = array();
139 foreach ($this->expect as $key => $value) {
140 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
141 // OK
142 } else if (is_null($value) && is_null($actual->$key)) {
143 // OK
144 } else {
145 $mismatches[] = $key;
146 }
147 }
148 return 'Actual object does not have all the same fields with the same values as the expected object (' .
149 implode(', ', $mismatches) . ').';
150 }
151}
152
58fa5d6f 153class FakeDBUnitTestCase extends UnitTestCase {
356e0010 154 public $tables = array();
90997d6d 155 public $pkfile;
156 public $cfg;
274e2947 157 public $DB;
356e0010 158
90997d6d 159 /**
160 * In the constructor, record the max(id) of each test table into a csv file.
161 * If this file already exists, it means that a previous run of unit tests
162 * did not complete, and has left data undeleted in the DB. This data is then
163 * deleted and the file is retained. Otherwise it is created.
164 * @throws moodle_exception if CSV file cannot be created
165 */
b9c639d6 166 public function __construct($label = false) {
84ebf08d 167 global $DB, $CFG;
168
169 if (empty($CFG->unittestprefix)) {
170 return;
171 }
172
b9c639d6 173 parent::UnitTestCase($label);
90997d6d 174 // MDL-16483 Get PKs and save data to text file
84ebf08d 175
90997d6d 176 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
177 $this->cfg = $CFG;
274e2947 178
179 UnitTestDB::instantiate();
90997d6d 180
181 $tables = $DB->get_tables();
182
183 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
184 if (file_exists($this->pkfile)) {
185 $this->truncate_test_tables($this->get_table_data($this->pkfile));
186
187 } else { // Create the file
188 $tabledata = '';
189
190 foreach ($tables as $table) {
58fa5d6f 191 if ($table != 'sessions') {
90997d6d 192 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
193 $max_id = 0;
194 }
195 $tabledata .= "$table, $max_id\n";
196 }
197 }
198 if (!file_put_contents($this->pkfile, $tabledata)) {
5d1381c2 199 $a = new stdClass();
200 $a->filename = $this->pkfile;
201 throw new moodle_exception('testtablescsvfileunwritable', 'simpletest', '', $a);
90997d6d 202 }
203 }
204 }
205
206 /**
207 * 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.
208 * @param array $tabledata
209 */
210 private function truncate_test_tables($tabledata) {
211 global $CFG, $DB;
212
84ebf08d 213 if (empty($CFG->unittestprefix)) {
214 return;
215 }
216
90997d6d 217 $tables = $DB->get_tables();
218
219 foreach ($tables as $table) {
3f57bd45 220 if ($table != 'sessions' && isset($tabledata[$table])) {
5d1381c2 221 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
90997d6d 222 }
223 }
356e0010 224 }
225
90997d6d 226 /**
227 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
228 * 1. Table name
229 * 2. Max id
230 * @param string $filename
231 * @throws moodle_exception if file doesn't exist
232 */
233 public function get_table_data($filename) {
84ebf08d 234 global $CFG;
235
236 if (empty($CFG->unittestprefix)) {
237 return;
238 }
239
90997d6d 240 if (file_exists($this->pkfile)) {
241 $handle = fopen($this->pkfile, 'r');
242 $tabledata = array();
243
244 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
245 $tabledata[$data[0]] = $data[1];
246 }
247 return $tabledata;
248 } else {
5d1381c2 249 $a = new stdClass();
250 $a->filename = $this->pkfile;
251 debug_print_backtrace();
252 throw new moodle_exception('testtablescsvfilemissing', 'simpletest', '', $a);
90997d6d 253 return false;
254 }
255 }
256
257 /**
258 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
259 * Also detects if this config setting is properly set, and if the user table exists.
260 * TODO Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
261 */
356e0010 262 public function setUp() {
84ebf08d 263 global $DB, $CFG;
264
265 if (empty($CFG->unittestprefix)) {
266 return;
267 }
268
356e0010 269 parent::setUp();
274e2947 270 $this->DB =& $DB;
915d745f 271 ob_start();
5d1381c2 272 }
273
274 /**
275 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
276 */
277 public function tearDown() {
84ebf08d 278 global $DB, $CFG;
279
280 if (empty($CFG->unittestprefix)) {
281 return;
282 }
283
274e2947 284 if (empty($DB)) {
285 $DB = $this->DB;
286 }
5d1381c2 287 $DB->cleanup();
288 parent::tearDown();
747f4a4c 289
290 // Output buffering
915d745f 291 if (ob_get_length() > 0) {
292 ob_end_flush();
293 }
5d1381c2 294 }
295
296 /**
297 * 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
298 * It should also detect if data is missing from the original tables.
299 */
300 public function __destruct() {
301 global $CFG, $DB;
302
84ebf08d 303 if (empty($CFG->unittestprefix)) {
304 return;
305 }
306
5d1381c2 307 $CFG = $this->cfg;
308 $this->tearDown();
309 UnitTestDB::restore();
310 fulldelete($this->pkfile);
311 }
821e4ecf 312
313 /**
314 * Load a table with some rows of data. A typical call would look like:
315 *
316 * $config = $this->load_test_data('config_plugins',
317 * array('plugin', 'name', 'value'), array(
318 * array('frog', 'numlegs', 2),
319 * array('frog', 'sound', 'croak'),
320 * array('frog', 'action', 'jump'),
321 * ));
322 *
323 * @param string $table the table name.
324 * @param array $cols the columns to fill.
325 * @param array $data the data to load.
326 * @return array $objects corresponding to $data.
327 */
328 public function load_test_data($table, array $cols, array $data) {
84ebf08d 329 global $CFG, $DB;
330
331 if (empty($CFG->unittestprefix)) {
332 return;
333 }
334
821e4ecf 335 $results = array();
336 foreach ($data as $rowid => $row) {
337 $obj = new stdClass;
338 foreach ($cols as $key => $colname) {
339 $obj->$colname = $row[$key];
340 }
341 $obj->id = $DB->insert_record($table, $obj);
342 $results[$rowid] = $obj;
343 }
344 return $results;
345 }
346
347 /**
348 * Clean up data loaded with load_test_data. The call corresponding to the
349 * example load above would be:
350 *
351 * $this->delete_test_data('config_plugins', $config);
352 *
353 * @param string $table the table name.
354 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
355 */
356 public function delete_test_data($table, array $rows) {
84ebf08d 357 global $CFG, $DB;
358
359 if (empty($CFG->unittestprefix)) {
360 return;
361 }
362
821e4ecf 363 $ids = array();
364 foreach ($rows as $row) {
365 $ids[] = $row->id;
366 }
367 $DB->delete_records_list($table, 'id', $ids);
368 }
5d1381c2 369}
370
371/**
372 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
373 * static instantiate() method, and restores the original global $DB through restore().
374 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
375 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
376 * without subclassing it.
377 */
378class UnitTestDB {
379 public static $DB;
380 private static $real_db;
356e0010 381
5d1381c2 382 public $table_data = array();
383
5d1381c2 384 /**
385 * Call this statically to connect to the DB using the unittest prefix, instantiate
386 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
387 */
388 public static function instantiate() {
389 global $CFG, $DB;
390 UnitTestDB::$real_db = clone($DB);
90997d6d 391 if (empty($CFG->unittestprefix)) {
b9c639d6 392 print_error("prefixnotset", 'simpletest');
393 }
394
274e2947 395 if (empty(UnitTestDB::$DB)) {
396 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
beaa43db 397 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
274e2947 398 }
399
5d1381c2 400 $manager = UnitTestDB::$DB->get_manager();
356e0010 401
402 if (!$manager->table_exists('user')) {
b9c639d6 403 print_error('tablesnotsetup', 'simpletest');
404 }
5d1381c2 405
406 $DB = new UnitTestDB();
407 }
408
409 public function __call($method, $args) {
410 // Set args to null if they don't exist (up to 10 args should do)
411 if (!method_exists($this, $method)) {
412 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
413 } else {
414 call_user_func_array(array($this, $method), $args);
415 }
416 }
417
418 public function __get($variable) {
419 return UnitTestDB::$DB->$variable;
420 }
421
422 public function __set($variable, $value) {
423 UnitTestDB::$DB->$variable = $value;
424 }
425
426 public function __isset($variable) {
427 return isset(UnitTestDB::$DB->$variable);
428 }
429
430 public function __unset($variable) {
431 unset(UnitTestDB::$DB->$variable);
b9c639d6 432 }
433
90997d6d 434 /**
5d1381c2 435 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
90997d6d 436 */
5d1381c2 437 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
356e0010 438 global $DB;
5d1381c2 439 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
440 $this->table_data[$table][] = $id;
441 return $id;
442 }
356e0010 443
5d1381c2 444 /**
445 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
446 * throw an exception and cancel update.
447 * @throws moodle_exception If trying to update a record not inserted by unit tests.
448 */
449 public function update_record($table, $dataobject, $bulk=false) {
450 global $DB;
747f4a4c 451 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
274e2947 452 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
453 $a = new stdClass();
454 $a->id = $dataobject->id;
455 $a->table = $table;
456 debug_print_backtrace();
457 throw new moodle_exception('updatingnoninsertedrecord', 'simpletest', '', $a);
5d1381c2 458 } else {
459 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
460 }
b9c639d6 461 }
462
463 /**
5d1381c2 464 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
465 * throw an exception and cancel delete.
466 * @throws moodle_exception If trying to delete a record not inserted by unit tests.
b9c639d6 467 */
274e2947 468 public function delete_records($table, array $conditions=array()) {
5d1381c2 469 global $DB;
747f4a4c 470 $tables_to_ignore = array('context_temp');
471
5d1381c2 472 $a = new stdClass();
473 $a->table = $table;
474
475 // Get ids matching conditions
476 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
477 return UnitTestDB::$DB->delete_records($table, $conditions);
478 }
479
480 $proceed_with_delete = true;
481
482 if (!is_array($ids_to_delete)) {
483 $ids_to_delete = array($ids_to_delete);
484 }
485
486 foreach ($ids_to_delete as $id) {
747f4a4c 487 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
5d1381c2 488 $proceed_with_delete = false;
489 $a->id = $id;
490 break;
491 }
492 }
493
494 if ($proceed_with_delete) {
495 return UnitTestDB::$DB->delete_records($table, $conditions);
496 } else {
497 debug_print_backtrace();
498 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
499 }
b9c639d6 500 }
b9c639d6 501
5d1381c2 502 /**
503 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
504 * throw an exception and cancel delete.
505 * @throws moodle_exception If trying to delete a record not inserted by unit tests.
506 */
507 public function delete_records_select($table, $select, array $params=null) {
508 global $DB;
509 $a = new stdClass();
510 $a->table = $table;
511
512 // Get ids matching conditions
513 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
514 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
515 }
516
517 $proceed_with_delete = true;
518
519 foreach ($ids_to_delete as $id) {
520 if (!in_array($id, $this->table_data[$table])) {
521 $proceed_with_delete = false;
522 $a->id = $id;
523 break;
524 }
525 }
526
527 if ($proceed_with_delete) {
528 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
529 } else {
530 debug_print_backtrace();
531 throw new moodle_exception('deletingnoninsertedrecord', 'simpletest', '', $a);
532 }
533 }
534
535 /**
536 * Removes from the test DB all the records that were inserted during unit tests,
537 */
538 public function cleanup() {
539 global $DB;
540 foreach ($this->table_data as $table => $ids) {
541 foreach ($ids as $id) {
542 $DB->delete_records($table, array('id' => $id));
543 }
544 }
545 }
546
547 /**
548 * Restores the global $DB object.
549 */
550 public static function restore() {
551 global $DB;
552 $DB = UnitTestDB::$real_db;
553 }
554
555 public function get_field($table, $return, array $conditions) {
556 if (!is_array($conditions)) {
557 debug_print_backtrace();
558 }
559 return UnitTestDB::$DB->get_field($table, $return, $conditions);
560 }
561}
bb48a537 562?>