MDL-32400 store version hash also in database
[moodle.git] / lib / phpunit / lib.php
CommitLineData
5bd40408
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Various PHPUnit classes and functions
19 *
7aea08e1 20 * @package core
5bd40408
PS
21 * @category phpunit
22 * @copyright 2012 Petr Skoda {@link http://skodak.org}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
7aea08e1 26require_once 'PHPUnit/Autoload.php';
e396e8ed 27require_once 'PHPUnit/Extensions/Database/Autoload.php';
5bd40408
PS
28
29
30/**
31 * Collection of utility methods.
32 *
7aea08e1
SH
33 * @package core
34 * @category phpunit
5bd40408
PS
35 * @copyright 2012 Petr Skoda {@link http://skodak.org}
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class phpunit_util {
39 /**
40 * @var array original content of all database tables
41 */
42 protected static $tabledata = null;
43
0b9251e3
PS
44 /**
45 * @var array original structure of all database tables
46 */
47 protected static $tablestructure = null;
48
7aea08e1 49 /**
920f4efe 50 * @var array An array of original globals, restored after each test
7aea08e1 51 */
5bd40408
PS
52 protected static $globals = array();
53
a3d5830a
PS
54 /**
55 * @var int last value of db writes counter, used for db resetting
56 */
50be93d1 57 public static $lastdbwrites = null;
a3d5830a
PS
58
59 /**
60 * @var phpunit_data_generator
61 */
62 protected static $generator = null;
63
920f4efe
PS
64 /**
65 * @var resource used for prevention of parallel test execution
66 */
4be2ad36
PS
67 protected static $lockhandle = null;
68
69 /**
920f4efe 70 * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
4be2ad36
PS
71 *
72 * Note: do not call manually!
73 *
920f4efe 74 * @internal
4be2ad36
PS
75 * @static
76 * @return void
77 */
78 public static function acquire_test_lock() {
79 global $CFG;
6c583c75
PS
80 if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
81 // dataroot not initialised yet
82 return;
83 }
4be2ad36
PS
84 if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) {
85 file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests');
86 phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock");
87 }
88 if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) {
89 $wouldblock = null;
90 $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock);
e3701541
PS
91 if (!$locked) {
92 if ($wouldblock) {
93 echo "Waiting for other test execution to complete...\n";
94 }
4be2ad36
PS
95 $locked = flock(self::$lockhandle, LOCK_EX);
96 }
97 if (!$locked) {
98 fclose(self::$lockhandle);
99 self::$lockhandle = null;
100 }
101 }
102 register_shutdown_function(array('phpunit_util', 'release_test_lock'));
103 }
104
105 /**
106 * Note: do not call manually!
920f4efe 107 * @internal
4be2ad36
PS
108 * @static
109 * @return void
110 */
111 public static function release_test_lock() {
112 if (self::$lockhandle) {
113 flock(self::$lockhandle, LOCK_UN);
114 fclose(self::$lockhandle);
115 self::$lockhandle = null;
116 }
117 }
118
a3d5830a
PS
119 /**
120 * Get data generator
121 * @static
122 * @return phpunit_data_generator
123 */
124 public static function get_data_generator() {
125 if (is_null(self::$generator)) {
126 require_once(__DIR__.'/generatorlib.php');
127 self::$generator = new phpunit_data_generator();
128 }
129 return self::$generator;
130 }
131
5bd40408
PS
132 /**
133 * Returns contents of all tables right after installation.
134 * @static
135 * @return array $table=>$records
136 */
137 protected static function get_tabledata() {
138 global $CFG;
139
a3d5830a
PS
140 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
141 // not initialised yet
142 return array();
143 }
144
5bd40408
PS
145 if (!isset(self::$tabledata)) {
146 $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
147 self::$tabledata = unserialize($data);
148 }
149
150 if (!is_array(self::$tabledata)) {
7b0ff213 151 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
5bd40408
PS
152 }
153
154 return self::$tabledata;
155 }
156
0b9251e3
PS
157 /**
158 * Returns structure of all tables right after installation.
159 * @static
160 * @return array $table=>$records
161 */
e396e8ed 162 public static function get_tablestructure() {
0b9251e3
PS
163 global $CFG;
164
165 if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
166 // not initialised yet
167 return array();
168 }
169
170 if (!isset(self::$tablestructure)) {
171 $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
172 self::$tablestructure = unserialize($data);
173 }
174
175 if (!is_array(self::$tablestructure)) {
176 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.');
177 }
178
179 return self::$tablestructure;
180 }
181
e10736b3
PS
182 /**
183 * Returns list of tables that are unmodified and empty.
184 *
185 * @static
186 * @return array of table names, empty if unknown
187 */
188 protected static function guess_unmodified_empty_tables() {
189 global $DB;
190
191 $dbfamily = $DB->get_dbfamily();
192
193 if ($dbfamily === 'mysql') {
194 $empties = array();
195 $prefix = $DB->get_prefix();
196 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
197 foreach ($rs as $info) {
198 $table = strtolower($info->name);
199 if (strpos($table, $prefix) !== 0) {
200 // incorrect table match caused by _
201 continue;
202 }
203 if (!is_null($info->auto_increment)) {
204 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
205 if ($info->auto_increment == 1) {
206 $empties[$table] = $table;
207 }
208 }
209 }
210 $rs->close();
211 return $empties;
212
5a798e7e
PS
213 } else if ($dbfamily === 'mssql') {
214 $empties = array();
215 $prefix = $DB->get_prefix();
216 $sql = "SELECT t.name
217 FROM sys.identity_columns i
218 JOIN sys.tables t ON t.object_id = i.object_id
219 WHERE t.name LIKE ?
220 AND i.name = 'id'
221 AND i.last_value IS NULL";
222 $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
223 foreach ($rs as $info) {
224 $table = strtolower($info->name);
225 if (strpos($table, $prefix) !== 0) {
226 // incorrect table match caused by _
227 continue;
228 }
229 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
230 $empties[$table] = $table;
231 }
232 $rs->close();
233 return $empties;
234
e10736b3
PS
235 } else {
236 return array();
237 }
238 }
239
8b5413cc 240 /**
ab483c0a
PS
241 * Reset all database sequences to initial values.
242 *
8b5413cc 243 * @static
e10736b3 244 * @param array $empties tables that are known to be unmodified and empty
8b5413cc
PS
245 * @return void
246 */
e10736b3 247 public static function reset_all_database_sequences(array $empties = null) {
8b5413cc
PS
248 global $DB;
249
8b5413cc
PS
250 if (!$data = self::get_tabledata()) {
251 // not initialised yet
ab483c0a 252 return;
8b5413cc
PS
253 }
254 if (!$structure = self::get_tablestructure()) {
255 // not initialised yet
ab483c0a 256 return;
8b5413cc
PS
257 }
258
259 $dbfamily = $DB->get_dbfamily();
260 if ($dbfamily === 'postgres') {
261 $queries = array();
262 $prefix = $DB->get_prefix();
263 foreach ($data as $table=>$records) {
ab483c0a 264 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
8b5413cc
PS
265 if (empty($records)) {
266 $nextid = 1;
267 } else {
268 $lastrecord = end($records);
269 $nextid = $lastrecord->id + 1;
270 }
271 $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
272 }
273 }
274 if ($queries) {
275 $DB->change_database_structure(implode(';', $queries));
276 }
8b5413cc 277
ab483c0a
PS
278 } else if ($dbfamily === 'mysql') {
279 $sequences = array();
8b5413cc
PS
280 $prefix = $DB->get_prefix();
281 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
282 foreach ($rs as $info) {
283 $table = strtolower($info->name);
284 if (strpos($table, $prefix) !== 0) {
285 // incorrect table match caused by _
286 continue;
287 }
288 if (!is_null($info->auto_increment)) {
e10736b3 289 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
8b5413cc
PS
290 $sequences[$table] = $info->auto_increment;
291 }
292 }
293 $rs->close();
ab483c0a
PS
294 foreach ($data as $table=>$records) {
295 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
296 if (isset($sequences[$table])) {
297 if (empty($records)) {
298 $lastid = 0;
299 } else {
300 $lastrecord = end($records);
301 $lastid = $lastrecord->id;
302 }
303 if ($sequences[$table] != $lastid +1) {
304 $DB->get_manager()->reset_sequence($table);
305 }
8b5413cc 306
8b5413cc 307 } else {
8b5413cc
PS
308 $DB->get_manager()->reset_sequence($table);
309 }
ab483c0a
PS
310 }
311 }
8b5413cc 312
ab483c0a
PS
313 } else {
314 // note: does mssql and oracle support any kind of faster reset?
e10736b3
PS
315 if (is_null($empties)) {
316 $empties = self::guess_unmodified_empty_tables();
317 }
ab483c0a 318 foreach ($data as $table=>$records) {
e10736b3
PS
319 if (isset($empties[$table])) {
320 continue;
321 }
ab483c0a 322 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
8b5413cc
PS
323 $DB->get_manager()->reset_sequence($table);
324 }
325 }
326 }
327 }
328
5bd40408 329 /**
a3d5830a 330 * Reset all database tables to default values.
5bd40408 331 * @static
a3d5830a 332 * @return bool true if reset done, false if skipped
5bd40408 333 */
7b0ff213 334 public static function reset_database() {
a3d5830a
PS
335 global $DB;
336
a3d5830a
PS
337 $tables = $DB->get_tables(false);
338 if (!$tables or empty($tables['config'])) {
339 // not installed yet
fe35ccaf 340 return false;
5bd40408
PS
341 }
342
fe35ccaf
PS
343 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
344 return false;
345 }
346 if (!$data = self::get_tabledata()) {
347 // not initialised yet
348 return false;
349 }
0b9251e3
PS
350 if (!$structure = self::get_tablestructure()) {
351 // not initialised yet
352 return false;
353 }
728eadac 354
e10736b3
PS
355 $empties = self::guess_unmodified_empty_tables();
356
fe35ccaf
PS
357 foreach ($data as $table=>$records) {
358 if (empty($records)) {
e10736b3
PS
359 if (isset($empties[$table])) {
360 // table was not modified and is empty
361 } else {
362 $DB->delete_records($table, array());
363 }
fe35ccaf
PS
364 continue;
365 }
366
ab483c0a 367 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
714f3998
PS
368 $currentrecords = $DB->get_records($table, array(), 'id ASC');
369 $changed = false;
370 foreach ($records as $id=>$record) {
371 if (!isset($currentrecords[$id])) {
372 $changed = true;
373 break;
374 }
375 if ((array)$record != (array)$currentrecords[$id]) {
376 $changed = true;
377 break;
728eadac 378 }
714f3998
PS
379 unset($currentrecords[$id]);
380 }
381 if (!$changed) {
382 if ($currentrecords) {
383 $lastrecord = end($records);
384 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
385 continue;
386 } else {
387 continue;
a3d5830a 388 }
a3d5830a 389 }
fe35ccaf 390 }
728eadac 391
fe35ccaf 392 $DB->delete_records($table, array());
fe35ccaf
PS
393 foreach ($records as $record) {
394 $DB->import_record($table, $record, false, true);
395 }
396 }
714f3998 397
8b5413cc 398 // reset all next record ids - aka sequences
e10736b3 399 self::reset_all_database_sequences($empties);
728eadac 400
fe35ccaf 401 // remove extra tables
714f3998
PS
402 foreach ($tables as $table) {
403 if (!isset($data[$table])) {
404 $DB->get_manager()->drop_table(new xmldb_table($table));
5bd40408 405 }
5bd40408 406 }
a3d5830a
PS
407
408 self::$lastdbwrites = $DB->perf_get_writes();
409
fe35ccaf 410 return true;
5bd40408
PS
411 }
412
728eadac 413 /**
920f4efe 414 * Purge dataroot directory
728eadac
PS
415 * @static
416 * @return void
417 */
418 public static function reset_dataroot() {
419 global $CFG;
420
421 $handle = opendir($CFG->dataroot);
422 $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
423 while (false !== ($item = readdir($handle))) {
424 if (in_array($item, $skip)) {
425 continue;
426 }
427 if (is_dir("$CFG->dataroot/$item")) {
428 remove_dir("$CFG->dataroot/$item", false);
429 } else {
430 unlink("$CFG->dataroot/$item");
431 }
432 }
433 closedir($handle);
434 make_temp_directory('');
435 make_cache_directory('');
436 make_cache_directory('htmlpurifier');
437 }
438
5bd40408
PS
439 /**
440 * Reset contents of all database tables to initial values, reset caches, etc.
441 *
442 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
443 *
920f4efe 444 * @static
a3d5830a 445 * @param bool $logchanges log changes in global state and database in error log
a3d5830a 446 * @return void
5bd40408 447 */
7b0ff213 448 public static function reset_all_data($logchanges = false) {
458b3386 449 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION;
5bd40408 450
95dcf965
PS
451 // reset global $DB in case somebody mocked it
452 $DB = self::get_global_backup('DB');
453
ce9f3beb
PS
454 if ($DB->is_transaction_started()) {
455 // we can not reset inside transaction
456 $DB->force_transaction_rollback();
457 }
458
fe35ccaf 459 $resetdb = self::reset_database();
7b0ff213 460 $warnings = array();
5bd40408 461
a3d5830a 462 if ($logchanges) {
fe35ccaf 463 if ($resetdb) {
6e2cff2d 464 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
a3d5830a
PS
465 }
466
467 $oldcfg = self::get_global_backup('CFG');
458b3386 468 $oldsite = self::get_global_backup('SITE');
a3d5830a
PS
469 foreach($CFG as $k=>$v) {
470 if (!property_exists($oldcfg, $k)) {
6e2cff2d 471 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
a3d5830a 472 } else if ($oldcfg->$k !== $CFG->$k) {
6e2cff2d 473 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
a3d5830a
PS
474 }
475 unset($oldcfg->$k);
476
477 }
478 if ($oldcfg) {
479 foreach($oldcfg as $k=>$v) {
6e2cff2d 480 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
5bd40408 481 }
5bd40408 482 }
a3d5830a
PS
483
484 if ($USER->id != 0) {
6e2cff2d 485 $warnings[] = 'Warning: unexpected change of $USER';
5bd40408 486 }
458b3386
PS
487
488 if ($COURSE->id != $oldsite->id) {
6e2cff2d 489 $warnings[] = 'Warning: unexpected change of $COURSE';
458b3386 490 }
5bd40408 491 }
5bd40408 492
920f4efe 493 // restore original globals
6e2cff2d 494 $_SERVER = self::get_global_backup('_SERVER');
a3d5830a 495 $CFG = self::get_global_backup('CFG');
458b3386
PS
496 $SITE = self::get_global_backup('SITE');
497 $COURSE = $SITE;
498
920f4efe 499 // reinitialise following globals
458b3386
PS
500 $OUTPUT = new bootstrap_renderer();
501 $PAGE = new moodle_page();
502 $FULLME = null;
503 $ME = null;
504 $SCRIPT = null;
505 $SESSION = new stdClass();
506 $_SESSION['SESSION'] =& $SESSION;
5bd40408 507
920f4efe 508 // set fresh new not-logged-in user
5bd40408
PS
509 $user = new stdClass();
510 $user->id = 0;
5bd40408
PS
511 $user->mnethostid = $CFG->mnet_localhost_id;
512 session_set_user($user);
a3d5830a
PS
513
514 // reset all static caches
515 accesslib_clear_all_caches(true);
516 get_string_manager()->reset_caches();
812013b1 517 events_get_handlers('reset');
a3d5830a
PS
518 //TODO: add more resets here and probably refactor them to new core function
519
920f4efe 520 // purge dataroot directory
728eadac 521 self::reset_dataroot();
a3d5830a 522
920f4efe 523 // restore original config once more in case resetting of caches changed CFG
a3d5830a
PS
524 $CFG = self::get_global_backup('CFG');
525
a3d5830a
PS
526 // inform data generator
527 self::get_data_generator()->reset();
528
529 // fix PHP settings
530 error_reporting($CFG->debug);
7b0ff213 531
8b5413cc
PS
532 // verify db writes just in case something goes wrong in reset
533 if (self::$lastdbwrites != $DB->perf_get_writes()) {
920f4efe 534 error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
8b5413cc
PS
535 self::$lastdbwrites = $DB->perf_get_writes();
536 }
537
7b0ff213
PS
538 if ($warnings) {
539 $warnings = implode("\n", $warnings);
540 trigger_error($warnings, E_USER_WARNING);
541 }
5bd40408
PS
542 }
543
544 /**
545 * Called during bootstrap only!
920f4efe 546 * @internal
5bd40408 547 * @static
920f4efe 548 * @return void
5bd40408 549 */
a3d5830a 550 public static function bootstrap_init() {
95dcf965 551 global $CFG, $SITE, $DB;
5bd40408 552
a3d5830a 553 // backup the globals
6e2cff2d 554 self::$globals['_SERVER'] = $_SERVER;
5bd40408 555 self::$globals['CFG'] = clone($CFG);
458b3386 556 self::$globals['SITE'] = clone($SITE);
95dcf965 557 self::$globals['DB'] = $DB;
a3d5830a
PS
558
559 // refresh data in all tables, clear caches, etc.
560 phpunit_util::reset_all_data();
5bd40408
PS
561 }
562
563 /**
564 * Returns original state of global variable.
565 * @static
566 * @param string $name
567 * @return mixed
568 */
569 public static function get_global_backup($name) {
95dcf965
PS
570 if ($name === 'DB') {
571 // no cloning of database object,
572 // we just need the original reference, not original state
573 return self::$globals['DB'];
574 }
5bd40408
PS
575 if (isset(self::$globals[$name])) {
576 if (is_object(self::$globals[$name])) {
577 $return = clone(self::$globals[$name]);
578 return $return;
579 } else {
580 return self::$globals[$name];
581 }
582 }
583 return null;
584 }
585
586 /**
587 * Does this site (db and dataroot) appear to be used for production?
588 * We try very hard to prevent accidental damage done to production servers!!
589 *
590 * @static
591 * @return bool
592 */
593 public static function is_test_site() {
594 global $DB, $CFG;
595
596 if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
597 // this is already tested in bootstrap script,
a3d5830a 598 // but anyway presence of this file means the dataroot is for testing
5bd40408
PS
599 return false;
600 }
601
602 $tables = $DB->get_tables(false);
603 if ($tables) {
604 if (!$DB->get_manager()->table_exists('config')) {
605 return false;
606 }
607 if (!get_config('core', 'phpunittest')) {
608 return false;
609 }
610 }
611
612 return true;
613 }
614
615 /**
616 * Is this site initialised to run unit tests?
617 *
618 * @static
7b0ff213 619 * @return int array errorcode=>message, 0 means ok
5bd40408 620 */
a3d5830a 621 public static function testing_ready_problem() {
7b0ff213 622 global $CFG, $DB;
5bd40408 623
7b0ff213 624 $tables = $DB->get_tables(false);
5bd40408 625
7b0ff213
PS
626 if (!self::is_test_site()) {
627 // dataroot was verified in bootstrap, so it must be DB
e2b8630b 628 return array(131, 'Can not use database for testing, try different prefix');
5bd40408
PS
629 }
630
de3d1590
PS
631 if (empty($tables)) {
632 return array(132, '');
633 }
634
0b9251e3 635 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
de3d1590 636 return array(133, '');
5bd40408
PS
637 }
638
639 if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
de3d1590 640 return array(133, '');
5bd40408
PS
641 }
642
643 $hash = phpunit_util::get_version_hash();
644 $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
645
646 if ($hash !== $oldhash) {
7b0ff213 647 return array(133, '');
5bd40408
PS
648 }
649
e2b8630b
PS
650 $dbhash = get_config('core', 'phpunittest');
651 if ($hash !== $dbhash) {
652 return array(133, '');
653 }
654
7b0ff213 655 return array(0, '');
5bd40408
PS
656 }
657
658 /**
659 * Drop all test site data.
660 *
661 * Note: To be used from CLI scripts only.
662 *
663 * @static
7aea08e1 664 * @return void may terminate execution with exit code
5bd40408
PS
665 */
666 public static function drop_site() {
667 global $DB, $CFG;
668
669 if (!self::is_test_site()) {
219d1a4e 670 phpunit_bootstrap_error(131, 'Can not drop non-test site!!');
5bd40408
PS
671 }
672
4be2ad36 673 // purge dataroot
728eadac 674 self::reset_dataroot();
5bd40408 675 phpunit_bootstrap_initdataroot($CFG->dataroot);
4be2ad36
PS
676 $keep = array('.', '..', 'lock', 'webrunner.xml');
677 $files = scandir("$CFG->dataroot/phpunit");
678 foreach ($files as $file) {
679 if (in_array($file, $keep)) {
680 continue;
681 }
682 $path = "$CFG->dataroot/phpunit/$file";
683 if (is_dir($path)) {
684 remove_dir($path, false);
685 } else {
686 unlink($path);
687 }
688 }
5bd40408
PS
689
690 // drop all tables
5bd40408 691 $tables = $DB->get_tables(false);
728eadac
PS
692 if (isset($tables['config'])) {
693 // config always last to prevent problems with interrupted drops!
694 unset($tables['config']);
695 $tables['config'] = 'config';
5bd40408 696 }
5bd40408
PS
697 foreach ($tables as $tablename) {
698 $table = new xmldb_table($tablename);
699 $DB->get_manager()->drop_table($table);
700 }
701 }
702
703 /**
704 * Perform a fresh test site installation
705 *
706 * Note: To be used from CLI scripts only.
707 *
708 * @static
7aea08e1 709 * @return void may terminate execution with exit code
5bd40408
PS
710 */
711 public static function install_site() {
712 global $DB, $CFG;
713
714 if (!self::is_test_site()) {
219d1a4e 715 phpunit_bootstrap_error(131, 'Can not install on non-test site!!');
5bd40408
PS
716 }
717
718 if ($DB->get_tables()) {
219d1a4e
PS
719 list($errorcode, $message) = phpunit_util::testing_ready_problem();
720 if ($errorcode) {
b6b8a193 721 phpunit_bootstrap_error(133, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
219d1a4e 722 } else {
b6b8a193 723 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
219d1a4e 724 }
5bd40408
PS
725 }
726
727 $options = array();
6e2cff2d 728 $options['adminpass'] = 'admin';
5bd40408
PS
729 $options['shortname'] = 'phpunit';
730 $options['fullname'] = 'PHPUnit test site';
731
732 install_cli_database($options, false);
733
a3d5830a
PS
734 // install timezone info
735 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
736 update_timezone_records($timezones);
5bd40408
PS
737
738 // add test db flag
e2b8630b
PS
739 $hash = phpunit_util::get_version_hash();
740 set_config('phpunittest', $hash);
5bd40408
PS
741
742 // store data for all tables
743 $data = array();
0b9251e3 744 $structure = array();
5bd40408
PS
745 $tables = $DB->get_tables();
746 foreach ($tables as $table) {
728eadac 747 $columns = $DB->get_columns($table);
0b9251e3 748 $structure[$table] = $columns;
ab483c0a 749 if (isset($columns['id']) and $columns['id']->auto_increment) {
728eadac
PS
750 $data[$table] = $DB->get_records($table, array(), 'id ASC');
751 } else {
752 // there should not be many of these
753 $data[$table] = $DB->get_records($table, array());
754 }
5bd40408
PS
755 }
756 $data = serialize($data);
5bd40408 757 file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
6e2cff2d 758 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
5bd40408 759
0b9251e3
PS
760 $structure = serialize($structure);
761 file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
762 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
763
5bd40408 764 // hash all plugin versions - helps with very fast detection of db structure changes
5bd40408 765 file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
6e2cff2d 766 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
5bd40408
PS
767 }
768
769 /**
920f4efe 770 * Calculate unique version hash for all plugins and core.
5bd40408
PS
771 * @static
772 * @return string sha1 hash
773 */
774 public static function get_version_hash() {
775 global $CFG;
776
777 $versions = array();
778
779 // main version first
780 $version = null;
781 include($CFG->dirroot.'/version.php');
782 $versions['core'] = $version;
783
784 // modules
785 $mods = get_plugin_list('mod');
786 ksort($mods);
787 foreach ($mods as $mod => $fullmod) {
788 $module = new stdClass();
789 $module->version = null;
790 include($fullmod.'/version.php');
791 $versions[$mod] = $module->version;
792 }
793
794 // now the rest of plugins
795 $plugintypes = get_plugin_types();
796 unset($plugintypes['mod']);
797 ksort($plugintypes);
798 foreach ($plugintypes as $type=>$unused) {
799 $plugs = get_plugin_list($type);
800 ksort($plugs);
801 foreach ($plugs as $plug=>$fullplug) {
802 $plugin = new stdClass();
803 $plugin->version = null;
804 @include($fullplug.'/version.php');
805 $versions[$plug] = $plugin->version;
806 }
807 }
808
809 $hash = sha1(serialize($versions));
810
811 return $hash;
812 }
813
814 /**
920f4efe 815 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
5bd40408 816 * @static
7b0ff213 817 * @return bool true means main config file created, false means only dataroot file created
5bd40408
PS
818 */
819 public static function build_config_file() {
820 global $CFG;
821
822 $template = '
5bd40408
PS
823 <testsuite name="@component@">
824 <directory suffix="_test.php">@dir@</directory>
a3d5830a 825 </testsuite>';
5bd40408
PS
826 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
827
828 $suites = '';
829
830 $plugintypes = get_plugin_types();
831 ksort($plugintypes);
832 foreach ($plugintypes as $type=>$unused) {
833 $plugs = get_plugin_list($type);
834 ksort($plugs);
835 foreach ($plugs as $plug=>$fullplug) {
836 if (!file_exists("$fullplug/tests/")) {
837 continue;
838 }
839 $dir = preg_replace("|$CFG->dirroot/|", '', $fullplug, 1);
840 $dir .= '/tests';
841 $component = $type.'_'.$plug;
842
843 $suite = str_replace('@component@', $component, $template);
844 $suite = str_replace('@dir@', $dir, $suite);
845
846 $suites .= $suite;
847 }
848 }
849
7aea08e1 850 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
5bd40408 851
7b0ff213
PS
852 $result = false;
853 if (is_writable($CFG->dirroot)) {
854 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
6e2cff2d 855 phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
7b0ff213
PS
856 }
857 }
6e2cff2d 858 // relink - it seems that xml:base does not work in phpunit xml files, remove this nasty hack if you find a way to set xml base for relative refs
7b0ff213
PS
859 $data = str_replace('lib/phpunit/', "$CFG->dirroot/lib/phpunit/", $data);
860 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|', '<directory suffix="_test.php">'.$CFG->dirroot.'/$1</directory>', $data);
861 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
6e2cff2d 862 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
7b0ff213
PS
863
864 return (bool)$result;
5bd40408
PS
865 }
866}
867
868
869/**
870 * Simplified emulation test case for legacy SimpleTest.
871 *
872 * Note: this is supposed to work for very simple tests only.
873 *
7aea08e1
SH
874 * @deprecated since 2.3
875 * @package core
876 * @category phpunit
5bd40408
PS
877 * @author Petr Skoda
878 * @copyright 2012 Petr Skoda {@link http://skodak.org}
879 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
880 */
d2999716 881abstract class UnitTestCase extends PHPUnit_Framework_TestCase {
5bd40408
PS
882
883 /**
7aea08e1 884 * @deprecated since 2.3
5bd40408
PS
885 * @param bool $expected
886 * @param string $message
a3d5830a 887 * @return void
5bd40408
PS
888 */
889 public function expectException($expected, $message = '') {
920f4efe 890 // alternatively use phpdocs: @expectedException ExceptionClassName
5bd40408
PS
891 if (!$expected) {
892 return;
893 }
894 $this->setExpectedException('moodle_exception', $message);
895 }
896
897 /**
7aea08e1 898 * @deprecated since 2.3
5bd40408
PS
899 * @param bool $expected
900 * @param string $message
a3d5830a 901 * @return void
5bd40408
PS
902 */
903 public static function expectError($expected = false, $message = '') {
904 // not available in PHPUnit
905 if (!$expected) {
906 return;
907 }
908 self::skipIf(true);
909 }
910
911 /**
7aea08e1 912 * @deprecated since 2.3
5bd40408
PS
913 * @static
914 * @param mixed $actual
915 * @param string $messages
a3d5830a 916 * @return void
5bd40408
PS
917 */
918 public static function assertTrue($actual, $messages = '') {
919 parent::assertTrue((bool)$actual, $messages);
920 }
921
922 /**
7aea08e1 923 * @deprecated since 2.3
5bd40408
PS
924 * @static
925 * @param mixed $actual
926 * @param string $messages
a3d5830a 927 * @return void
5bd40408
PS
928 */
929 public static function assertFalse($actual, $messages = '') {
930 parent::assertFalse((bool)$actual, $messages);
931 }
932
933 /**
7aea08e1 934 * @deprecated since 2.3
5bd40408
PS
935 * @static
936 * @param mixed $expected
937 * @param mixed $actual
938 * @param string $message
a3d5830a 939 * @return void
5bd40408
PS
940 */
941 public static function assertEqual($expected, $actual, $message = '') {
942 parent::assertEquals($expected, $actual, $message);
943 }
944
945 /**
7aea08e1 946 * @deprecated since 2.3
5bd40408
PS
947 * @static
948 * @param mixed $expected
949 * @param mixed $actual
950 * @param string $message
a3d5830a 951 * @return void
5bd40408
PS
952 */
953 public static function assertNotEqual($expected, $actual, $message = '') {
954 parent::assertNotEquals($expected, $actual, $message);
955 }
956
957 /**
7aea08e1 958 * @deprecated since 2.3
5bd40408
PS
959 * @static
960 * @param mixed $expected
961 * @param mixed $actual
962 * @param string $message
a3d5830a 963 * @return void
5bd40408
PS
964 */
965 public static function assertIdentical($expected, $actual, $message = '') {
966 parent::assertSame($expected, $actual, $message);
967 }
968
969 /**
7aea08e1 970 * @deprecated since 2.3
5bd40408
PS
971 * @static
972 * @param mixed $expected
973 * @param mixed $actual
974 * @param string $message
a3d5830a 975 * @return void
5bd40408
PS
976 */
977 public static function assertNotIdentical($expected, $actual, $message = '') {
978 parent::assertNotSame($expected, $actual, $message);
979 }
980
981 /**
7aea08e1 982 * @deprecated since 2.3
5bd40408
PS
983 * @static
984 * @param mixed $actual
985 * @param mixed $expected
986 * @param string $message
a3d5830a 987 * @return void
5bd40408
PS
988 */
989 public static function assertIsA($actual, $expected, $message = '') {
6e2cff2d
PS
990 if ($expected === 'array') {
991 parent::assertEquals(gettype($actual), 'array', $message);
992 } else {
993 parent::assertInstanceOf($expected, $actual, $message);
994 }
5bd40408
PS
995 }
996}
997
998
999/**
7aea08e1
SH
1000 * The simplest PHPUnit test case customised for Moodle
1001 *
a3d5830a 1002 * It is intended for isolated tests that do not modify database or any globals.
5bd40408 1003 *
7aea08e1
SH
1004 * @package core
1005 * @category phpunit
5bd40408
PS
1006 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1007 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1008 */
d2999716 1009abstract class basic_testcase extends PHPUnit_Framework_TestCase {
5bd40408
PS
1010
1011 /**
1012 * Constructs a test case with the given name.
1013 *
920f4efe 1014 * Note: use setUp() or setUpBeforeClass() in your test cases.
b0e980d7 1015 *
7aea08e1
SH
1016 * @param string $name
1017 * @param array $data
1018 * @param string $dataName
5bd40408 1019 */
b0e980d7 1020 final public function __construct($name = null, array $data = array(), $dataName = '') {
5bd40408
PS
1021 parent::__construct($name, $data, $dataName);
1022
1023 $this->setBackupGlobals(false);
1024 $this->setBackupStaticAttributes(false);
1025 $this->setRunTestInSeparateProcess(false);
5bd40408
PS
1026 }
1027
1028 /**
a3d5830a 1029 * Runs the bare test sequence and log any changes in global state or database.
5bd40408
PS
1030 * @return void
1031 */
39e2e9c4 1032 final public function runBare() {
7620602c
PS
1033 global $DB;
1034
713d2091
PS
1035 try {
1036 parent::runBare();
1037 } catch (Exception $e) {
1038 // cleanup after failed expectation
1039 phpunit_util::reset_all_data();
1040 throw $e;
1041 }
7620602c
PS
1042
1043 if ($DB->is_transaction_started()) {
7620602c
PS
1044 phpunit_util::reset_all_data();
1045 throw new coding_exception('basic_testcase '.$this->getName().' is not supposed to use database transactions!');
1046 }
1047
7b0ff213 1048 phpunit_util::reset_all_data(true);
a3d5830a
PS
1049 }
1050}
5bd40408 1051
a3d5830a
PS
1052
1053/**
1054 * Advanced PHPUnit test case customised for Moodle.
1055 *
1056 * @package core
1057 * @category phpunit
1058 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1059 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1060 */
d2999716 1061abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
458b3386 1062 /** @var bool automatically reset everything? null means log changes */
a0c5affe 1063 private $resetAfterTest;
a3d5830a 1064
50be93d1 1065 /** @var moodle_transaction */
a0c5affe 1066 private $testdbtransaction;
50be93d1 1067
a3d5830a
PS
1068 /**
1069 * Constructs a test case with the given name.
1070 *
920f4efe 1071 * Note: use setUp() or setUpBeforeClass() in your test cases.
b0e980d7 1072 *
a3d5830a
PS
1073 * @param string $name
1074 * @param array $data
1075 * @param string $dataName
1076 */
b0e980d7 1077 final public function __construct($name = null, array $data = array(), $dataName = '') {
a3d5830a
PS
1078 parent::__construct($name, $data, $dataName);
1079
1080 $this->setBackupGlobals(false);
1081 $this->setBackupStaticAttributes(false);
1082 $this->setRunTestInSeparateProcess(false);
1083 }
1084
1085 /**
1086 * Runs the bare test sequence.
1087 * @return void
1088 */
39e2e9c4 1089 final public function runBare() {
50be93d1
PS
1090 global $DB;
1091
8b5413cc
PS
1092 if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
1093 // this happens when previous test does not reset, we can not use transactions
1094 $this->testdbtransaction = null;
1095
5a798e7e 1096 } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
50be93d1
PS
1097 // database must allow rollback of DDL, so no mysql here
1098 $this->testdbtransaction = $DB->start_delegated_transaction();
1099 }
1100
713d2091
PS
1101 try {
1102 parent::runBare();
95dcf965
PS
1103 // set DB reference in case somebody mocked it in test
1104 $DB = phpunit_util::get_global_backup('DB');
713d2091
PS
1105 } catch (Exception $e) {
1106 // cleanup after failed expectation
713d2091
PS
1107 phpunit_util::reset_all_data();
1108 throw $e;
1109 }
5bd40408 1110
50be93d1
PS
1111 if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
1112 $this->testdbtransaction = null;
1113 }
50be93d1 1114
a3d5830a 1115 if ($this->resetAfterTest === true) {
8b5413cc 1116 if ($this->testdbtransaction) {
50be93d1 1117 $DB->force_transaction_rollback();
8b5413cc 1118 phpunit_util::reset_all_database_sequences();
50be93d1
PS
1119 phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
1120 }
714f3998 1121 phpunit_util::reset_all_data();
8b5413cc 1122
a3d5830a 1123 } else if ($this->resetAfterTest === false) {
8b5413cc
PS
1124 if ($this->testdbtransaction) {
1125 $this->testdbtransaction->allow_commit();
50be93d1 1126 }
a3d5830a 1127 // keep all data untouched for other tests
8b5413cc 1128
a3d5830a
PS
1129 } else {
1130 // reset but log what changed
8b5413cc 1131 if ($this->testdbtransaction) {
7620602c
PS
1132 try {
1133 $this->testdbtransaction->allow_commit();
1134 } catch (dml_transaction_exception $e) {
7620602c
PS
1135 phpunit_util::reset_all_data();
1136 throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
1137 }
50be93d1 1138 }
7b0ff213 1139 phpunit_util::reset_all_data(true);
a3d5830a 1140 }
7620602c
PS
1141
1142 // make sure test did not forget to close transaction
1143 if ($DB->is_transaction_started()) {
7620602c
PS
1144 phpunit_util::reset_all_data();
1145 if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
1146 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
1147 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
1148 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
1149 }
1150 }
a3d5830a
PS
1151 }
1152
e396e8ed
PS
1153 /**
1154 * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
1155 *
1156 * @param string $xmlFile
1157 * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet
1158 */
1159 protected function createFlatXMLDataSet($xmlFile) {
1160 return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile);
1161 }
1162
1163 /**
1164 * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
1165 *
1166 * @param string $xmlFile
1167 * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet
1168 */
1169 protected function createXMLDataSet($xmlFile) {
1170 return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile);
1171 }
1172
1173 /**
c691274b 1174 * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
e396e8ed
PS
1175 *
1176 * @param array $files array tablename=>cvsfile
1177 * @param string $delimiter
1178 * @param string $enclosure
1179 * @param string $escape
1180 * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet
1181 */
c691274b 1182 protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
e396e8ed
PS
1183 $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape);
1184 foreach($files as $table=>$file) {
1185 $dataSet->addTable($table, $file);
1186 }
1187 return $dataSet;
1188 }
1189
1190 /**
1191 * Creates new ArrayDataSet from given array
1192 *
1193 * @param array $data array of tables, first row in each table is columns
1194 * @return phpunit_ArrayDataSet
1195 */
1196 protected function createArrayDataSet(array $data) {
1197 return new phpunit_ArrayDataSet($data);
1198 }
1199
1200 /**
1201 * Load date into moodle database tables from standard PHPUnit data set.
1202 *
1203 * Note: it is usually better to use data generators
1204 *
1205 * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset
1206 * @return void
1207 */
1208 protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) {
1209 global $DB;
1210
1211 $structure = phpunit_util::get_tablestructure();
1212
1213 foreach($dataset->getTableNames() as $tablename) {
1214 $table = $dataset->getTable($tablename);
1215 $metadata = $dataset->getTableMetaData($tablename);
1216 $columns = $metadata->getColumns();
1217
1218 $doimport = false;
1219 if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
1220 $doimport = in_array('id', $columns);
1221 }
1222
1223 for($r=0; $r<$table->getRowCount(); $r++) {
1224 $record = $table->getRow($r);
1225 if ($doimport) {
1226 $DB->import_record($tablename, $record);
1227 } else {
1228 $DB->insert_record($tablename, $record);
1229 }
1230 }
1231 if ($doimport) {
1232 $DB->get_manager()->reset_sequence(new xmldb_table($tablename));
1233 }
1234 }
1235 }
1236
8b5413cc
PS
1237 /**
1238 * Call this method from test if you want to make sure that
1239 * the resetting of database is done the slow way without transaction
1240 * rollback.
920f4efe
PS
1241 *
1242 * This is useful especially when testing stuff that is not compatible with transactions.
1243 *
8b5413cc
PS
1244 * @return void
1245 */
1246 public function preventResetByRollback() {
1247 if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
1248 $this->testdbtransaction->allow_commit();
1249 $this->testdbtransaction = null;
1250 }
1251 }
1252
a3d5830a
PS
1253 /**
1254 * Reset everything after current test.
1255 * @param bool $reset true means reset state back, false means keep all data for the next test,
1256 * null means reset state and show warnings if anything changed
1257 * @return void
1258 */
1259 public function resetAfterTest($reset = true) {
1260 $this->resetAfterTest = $reset;
1261 }
1262
1263 /**
1264 * Cleanup after all tests are executed.
1265 *
1266 * Note: do not forget to call this if overridden...
1267 *
1268 * @static
1269 * @return void
1270 */
1271 public static function tearDownAfterClass() {
1272 phpunit_util::reset_all_data();
1273 }
1274
1275 /**
1276 * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
1277 * @static
1278 * @return void
1279 */
1280 public static function resetAllData() {
1281 phpunit_util::reset_all_data();
1282 }
1283
458b3386
PS
1284 /**
1285 * Set current $USER, reset access cache.
1286 * @static
e72ea4a5 1287 * @param null|int|stdClass $user user record, null means non-logged-in, integer means userid
458b3386
PS
1288 * @return void
1289 */
e72ea4a5
PS
1290 public static function setUser($user = null) {
1291 global $CFG, $DB;
458b3386 1292
e72ea4a5
PS
1293 if (is_object($user)) {
1294 $user = clone($user);
1295 } else if (!$user) {
458b3386
PS
1296 $user = new stdClass();
1297 $user->id = 0;
1298 $user->mnethostid = $CFG->mnet_localhost_id;
1299 } else {
e72ea4a5 1300 $user = $DB->get_record('user', array('id'=>$user));
458b3386
PS
1301 }
1302 unset($user->description);
1303 unset($user->access);
1304
1305 session_set_user($user);
1306 }
1307
a3d5830a
PS
1308 /**
1309 * Get data generator
1310 * @static
1311 * @return phpunit_data_generator
1312 */
1313 public static function getDataGenerator() {
1314 return phpunit_util::get_data_generator();
1315 }
1316
1317 /**
1318 * Recursively visit all the files in the source tree. Calls the callback
1319 * function with the pathname of each file found.
1320 *
6e2cff2d
PS
1321 * @param string $path the folder to start searching from.
1322 * @param string $callback the method of this class to call with the name of each file found.
1323 * @param string $fileregexp a regexp used to filter the search (optional).
1324 * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
a3d5830a
PS
1325 * only files that match the regexp will be included. (default false).
1326 * @param array $ignorefolders will not go into any of these folders (optional).
1327 * @return void
1328 */
1329 public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
1330 $files = scandir($path);
1331
1332 foreach ($files as $file) {
1333 $filepath = $path .'/'. $file;
1334 if (strpos($file, '.') === 0) {
1335 /// Don't check hidden files.
1336 continue;
1337 } else if (is_dir($filepath)) {
1338 if (!in_array($filepath, $ignorefolders)) {
1339 $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
1340 }
1341 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
1342 $this->$callback($filepath);
5bd40408 1343 }
a3d5830a
PS
1344 }
1345 }
1346}
5bd40408 1347
a3d5830a 1348
e396e8ed
PS
1349/**
1350 * based on array iterator code from PHPUnit documentation by Sebastian Bergmann
1351 * and added new constructor parameter for different array types.
1352 */
1353class phpunit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet {
1354 /**
1355 * @var array
1356 */
1357 protected $tables = array();
1358
1359 /**
1360 * @param array $data
1361 */
1362 public function __construct(array $data) {
1363 foreach ($data AS $tableName => $rows) {
1364 $firstrow = reset($rows);
1365
1366 if (array_key_exists(0, $firstrow)) {
1367 // columns in first row
1368 $columnsInFirstRow = true;
1369 $columns = $firstrow;
1370 $key = key($rows);
1371 unset($rows[$key]);
1372 } else {
1373 // column name is in each row as key
1374 $columnsInFirstRow = false;
1375 $columns = array_keys($firstrow);
1376 }
1377
1378 $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
1379 $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
1380
1381 foreach ($rows AS $row) {
1382 if ($columnsInFirstRow) {
1383 $row = array_combine($columns, $row);
1384 }
1385 $table->addRow($row);
1386 }
1387 $this->tables[$tableName] = $table;
1388 }
1389 }
1390
1391 protected function createIterator($reverse = FALSE) {
1392 return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
1393 }
1394
1395 public function getTable($tableName) {
1396 if (!isset($this->tables[$tableName])) {
1397 throw new InvalidArgumentException("$tableName is not a table in the current database.");
1398 }
1399
1400 return $this->tables[$tableName];
1401 }
1402}
1403
1404
a3d5830a 1405/**
458b3386
PS
1406 * Special test case for testing of DML drivers and DDL layer.
1407 *
c06563c8
PS
1408 * Note: Use only 'test_table*' names when creating new tables.
1409 *
1410 * For DML/DDL developers: you can add following settings to config.php if you want to test different driver than the main one,
1411 * the reason is to allow testing of incomplete drivers that do not allow full PHPUnit environment
1412 * initialisation (the database can be empty).
1413 * $CFG->phpunit_extra_drivers = array(
1414 * 1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'),
1415 * 2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'),
1416 * 3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'),
1417 * 4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'),
1418 * );
1419 * define('PHPUNIT_TEST_DRIVER')=1; //number is index in the previous array
a3d5830a
PS
1420 *
1421 * @package core
1422 * @category phpunit
1423 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1424 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1425 */
d2999716 1426abstract class database_driver_testcase extends PHPUnit_Framework_TestCase {
6e2cff2d 1427 /** @var moodle_database connection to extra database */
a0c5affe 1428 private static $extradb = null;
a3d5830a 1429
6e2cff2d 1430 /** @var moodle_database used in these tests*/
a3d5830a
PS
1431 protected $tdb;
1432
1433 /**
1434 * Constructs a test case with the given name.
1435 *
1436 * @param string $name
1437 * @param array $data
1438 * @param string $dataName
1439 */
b0e980d7 1440 final public function __construct($name = null, array $data = array(), $dataName = '') {
a3d5830a
PS
1441 parent::__construct($name, $data, $dataName);
1442
1443 $this->setBackupGlobals(false);
1444 $this->setBackupStaticAttributes(false);
1445 $this->setRunTestInSeparateProcess(false);
1446 }
1447
1448 public static function setUpBeforeClass() {
1449 global $CFG;
6e2cff2d 1450 parent::setUpBeforeClass();
a3d5830a
PS
1451
1452 if (!defined('PHPUNIT_TEST_DRIVER')) {
1453 // use normal $DB
1454 return;
5bd40408 1455 }
a3d5830a
PS
1456
1457 if (!isset($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER])) {
1458 throw new exception('Can not find driver configuration options with index: '.PHPUNIT_TEST_DRIVER);
1459 }
1460
1461 $dblibrary = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']) ? 'native' : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary'];
1462 $dbtype = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbtype'];
1463 $dbhost = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbhost'];
1464 $dbname = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbname'];
1465 $dbuser = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbuser'];
1466 $dbpass = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbpass'];
1467 $prefix = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['prefix'];
1468 $dboptions = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']) ? array() : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions'];
1469
1470 $classname = "{$dbtype}_{$dblibrary}_moodle_database";
1471 require_once("$CFG->libdir/dml/$classname.php");
1472 $d = new $classname();
1473 if (!$d->driver_installed()) {
1474 throw new exception('Database driver for '.$classname.' is not installed');
1475 }
1476
1477 $d->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
1478
1479 self::$extradb = $d;
1480 }
1481
1cbf2a20 1482 protected function setUp() {
a3d5830a 1483 global $DB;
6e2cff2d 1484 parent::setUp();
5bd40408 1485
a3d5830a
PS
1486 if (self::$extradb) {
1487 $this->tdb = self::$extradb;
1488 } else {
1489 $this->tdb = $DB;
5bd40408 1490 }
a3d5830a 1491 }
5bd40408 1492
1cbf2a20 1493 protected function tearDown() {
a3d5830a
PS
1494 // delete all test tables
1495 $dbman = $this->tdb->get_manager();
1496 $tables = $this->tdb->get_tables(false);
1497 foreach($tables as $tablename) {
1498 if (strpos($tablename, 'test_table') === 0) {
1499 $table = new xmldb_table($tablename);
1500 $dbman->drop_table($table);
1501 }
5bd40408 1502 }
6e2cff2d 1503 parent::tearDown();
a3d5830a 1504 }
5bd40408 1505
a3d5830a
PS
1506 public static function tearDownAfterClass() {
1507 if (self::$extradb) {
1508 self::$extradb->dispose();
1509 self::$extradb = null;
1510 }
1511 phpunit_util::reset_all_data();
6e2cff2d 1512 parent::tearDownAfterClass();
5bd40408 1513 }
a3d5830a 1514}