MDL-32323 add fast transaction based database reset for PostgreSQL
[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';
5bd40408
PS
27
28
29/**
30 * Collection of utility methods.
31 *
7aea08e1
SH
32 * @package core
33 * @category phpunit
5bd40408
PS
34 * @copyright 2012 Petr Skoda {@link http://skodak.org}
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 */
37class phpunit_util {
38 /**
39 * @var array original content of all database tables
40 */
41 protected static $tabledata = null;
42
0b9251e3
PS
43 /**
44 * @var array original structure of all database tables
45 */
46 protected static $tablestructure = null;
47
7aea08e1
SH
48 /**
49 * @var array An array of globals cloned from CFG
50 */
5bd40408
PS
51 protected static $globals = array();
52
a3d5830a
PS
53 /**
54 * @var int last value of db writes counter, used for db resetting
55 */
50be93d1 56 public static $lastdbwrites = null;
a3d5830a
PS
57
58 /**
59 * @var phpunit_data_generator
60 */
61 protected static $generator = null;
62
4be2ad36
PS
63 protected static $lockhandle = null;
64
65 /**
66 * Prevent parallel test execution - this can not work in Moodle because we modify DB and dataroot.
67 *
68 * Note: do not call manually!
69 *
70 * @static
71 * @return void
72 */
73 public static function acquire_test_lock() {
74 global $CFG;
75 if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) {
76 file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests');
77 phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock");
78 }
79 if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) {
80 $wouldblock = null;
81 $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock);
82 if (!$locked and $wouldblock) {
83 echo "Waiting for other test execution to complete...\n";
84 $locked = flock(self::$lockhandle, LOCK_EX);
85 }
86 if (!$locked) {
87 fclose(self::$lockhandle);
88 self::$lockhandle = null;
89 }
90 }
91 register_shutdown_function(array('phpunit_util', 'release_test_lock'));
92 }
93
94 /**
95 * Note: do not call manually!
96 * @static
97 * @return void
98 */
99 public static function release_test_lock() {
100 if (self::$lockhandle) {
101 flock(self::$lockhandle, LOCK_UN);
102 fclose(self::$lockhandle);
103 self::$lockhandle = null;
104 }
105 }
106
a3d5830a
PS
107 /**
108 * Get data generator
109 * @static
110 * @return phpunit_data_generator
111 */
112 public static function get_data_generator() {
113 if (is_null(self::$generator)) {
114 require_once(__DIR__.'/generatorlib.php');
115 self::$generator = new phpunit_data_generator();
116 }
117 return self::$generator;
118 }
119
5bd40408
PS
120 /**
121 * Returns contents of all tables right after installation.
122 * @static
123 * @return array $table=>$records
124 */
125 protected static function get_tabledata() {
126 global $CFG;
127
a3d5830a
PS
128 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
129 // not initialised yet
130 return array();
131 }
132
5bd40408
PS
133 if (!isset(self::$tabledata)) {
134 $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
135 self::$tabledata = unserialize($data);
136 }
137
138 if (!is_array(self::$tabledata)) {
7b0ff213 139 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
5bd40408
PS
140 }
141
142 return self::$tabledata;
143 }
144
0b9251e3
PS
145 /**
146 * Returns structure of all tables right after installation.
147 * @static
148 * @return array $table=>$records
149 */
150 protected static function get_tablestructure() {
151 global $CFG;
152
153 if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
154 // not initialised yet
155 return array();
156 }
157
158 if (!isset(self::$tablestructure)) {
159 $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
160 self::$tablestructure = unserialize($data);
161 }
162
163 if (!is_array(self::$tablestructure)) {
164 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.');
165 }
166
167 return self::$tablestructure;
168 }
169
5bd40408 170 /**
a3d5830a 171 * Reset all database tables to default values.
5bd40408 172 * @static
a3d5830a 173 * @return bool true if reset done, false if skipped
5bd40408 174 */
7b0ff213 175 public static function reset_database() {
a3d5830a
PS
176 global $DB;
177
a3d5830a
PS
178 $tables = $DB->get_tables(false);
179 if (!$tables or empty($tables['config'])) {
180 // not installed yet
fe35ccaf 181 return false;
5bd40408
PS
182 }
183
fe35ccaf
PS
184 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
185 return false;
186 }
187 if (!$data = self::get_tabledata()) {
188 // not initialised yet
189 return false;
190 }
0b9251e3
PS
191 if (!$structure = self::get_tablestructure()) {
192 // not initialised yet
193 return false;
194 }
728eadac 195
fe35ccaf
PS
196 foreach ($data as $table=>$records) {
197 if (empty($records)) {
714f3998 198 $DB->delete_records($table, array());
fe35ccaf
PS
199 continue;
200 }
201
0b9251e3 202 if (isset($structure[$table]['id']) and $structure[$table]['id']->primary_key) {
714f3998
PS
203 $currentrecords = $DB->get_records($table, array(), 'id ASC');
204 $changed = false;
205 foreach ($records as $id=>$record) {
206 if (!isset($currentrecords[$id])) {
207 $changed = true;
208 break;
209 }
210 if ((array)$record != (array)$currentrecords[$id]) {
211 $changed = true;
212 break;
728eadac 213 }
714f3998
PS
214 unset($currentrecords[$id]);
215 }
216 if (!$changed) {
217 if ($currentrecords) {
218 $lastrecord = end($records);
219 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
220 continue;
221 } else {
222 continue;
a3d5830a 223 }
a3d5830a 224 }
fe35ccaf 225 }
728eadac 226
fe35ccaf 227 $DB->delete_records($table, array());
fe35ccaf
PS
228 foreach ($records as $record) {
229 $DB->import_record($table, $record, false, true);
230 }
231 }
714f3998
PS
232
233 // reset all sequences as fast as possible, it is usually faster to find out current value than to update all,
234 // please note we really must verify all tables because sometimes records are added and later removed,
235 // but in the next run we really want to start again from MAX(id)+1
236 $dbfamily = $DB->get_dbfamily();
237 $sequences = array();
238 if ($dbfamily === 'mysql') {
239 $prefix = $DB->get_prefix();
240 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
241 foreach ($rs as $info) {
242 $table = strtolower($info->name);
243 if (strpos($table, $prefix) !== 0) {
244 // incorrect table match caused by _
245 continue;
246 }
247 if (!is_null($info->auto_increment)) {
248 $table = preg_replace('/^'.preg_quote($prefix).'/', '', $table);
249 $sequences[$table] = $info->auto_increment;
250 }
251 }
252 $rs->close();
253 } else if ($dbfamily === 'postgres') {
254 foreach ($data as $table=>$records) {
255 if (isset($structure[$table]['id']) and $structure[$table]['id']->primary_key) {
256 $sequences[$table] = $DB->get_field_sql("SELECT last_value FROM {{$table}}_id_seq;");
257
258 }
259 }
260 }
261
262 foreach ($data as $table=>$records) {
0b9251e3 263 if (isset($structure[$table]['id']) and $structure[$table]['id']->primary_key) {
714f3998
PS
264 if (isset($sequences[$table])) {
265 if (empty($records)) {
266 $lastid = 0;
267 } else {
268 $lastrecord = end($records);
269 $lastid = $lastrecord->id;
270 }
271 if ($sequences[$table] != $lastid +1) {
272 $DB->get_manager()->reset_sequence($table);
273 }
274
275 } else {
276 $DB->get_manager()->reset_sequence($table);
277 }
0b9251e3 278 }
fe35ccaf 279 }
728eadac 280
fe35ccaf 281 // remove extra tables
714f3998
PS
282 foreach ($tables as $table) {
283 if (!isset($data[$table])) {
284 $DB->get_manager()->drop_table(new xmldb_table($table));
5bd40408 285 }
5bd40408 286 }
a3d5830a
PS
287
288 self::$lastdbwrites = $DB->perf_get_writes();
289
fe35ccaf 290 return true;
5bd40408
PS
291 }
292
728eadac
PS
293 /**
294 * Purge dataroot
295 * @static
296 * @return void
297 */
298 public static function reset_dataroot() {
299 global $CFG;
300
301 $handle = opendir($CFG->dataroot);
302 $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
303 while (false !== ($item = readdir($handle))) {
304 if (in_array($item, $skip)) {
305 continue;
306 }
307 if (is_dir("$CFG->dataroot/$item")) {
308 remove_dir("$CFG->dataroot/$item", false);
309 } else {
310 unlink("$CFG->dataroot/$item");
311 }
312 }
313 closedir($handle);
314 make_temp_directory('');
315 make_cache_directory('');
316 make_cache_directory('htmlpurifier');
317 }
318
5bd40408
PS
319 /**
320 * Reset contents of all database tables to initial values, reset caches, etc.
321 *
322 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
323 *
a3d5830a 324 * @param bool $logchanges log changes in global state and database in error log
a3d5830a 325 * @return void
5bd40408
PS
326 * @static
327 */
7b0ff213 328 public static function reset_all_data($logchanges = false) {
458b3386 329 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION;
5bd40408 330
fe35ccaf 331 $resetdb = self::reset_database();
7b0ff213 332 $warnings = array();
5bd40408 333
a3d5830a 334 if ($logchanges) {
fe35ccaf 335 if ($resetdb) {
6e2cff2d 336 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
a3d5830a
PS
337 }
338
339 $oldcfg = self::get_global_backup('CFG');
458b3386 340 $oldsite = self::get_global_backup('SITE');
a3d5830a
PS
341 foreach($CFG as $k=>$v) {
342 if (!property_exists($oldcfg, $k)) {
6e2cff2d 343 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
a3d5830a 344 } else if ($oldcfg->$k !== $CFG->$k) {
6e2cff2d 345 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
a3d5830a
PS
346 }
347 unset($oldcfg->$k);
348
349 }
350 if ($oldcfg) {
351 foreach($oldcfg as $k=>$v) {
6e2cff2d 352 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
5bd40408 353 }
5bd40408 354 }
a3d5830a
PS
355
356 if ($USER->id != 0) {
6e2cff2d 357 $warnings[] = 'Warning: unexpected change of $USER';
5bd40408 358 }
458b3386
PS
359
360 if ($COURSE->id != $oldsite->id) {
6e2cff2d 361 $warnings[] = 'Warning: unexpected change of $COURSE';
458b3386 362 }
5bd40408 363 }
5bd40408 364
a3d5830a 365 // restore original config
6e2cff2d 366 $_SERVER = self::get_global_backup('_SERVER');
a3d5830a 367 $CFG = self::get_global_backup('CFG');
458b3386
PS
368 $SITE = self::get_global_backup('SITE');
369 $COURSE = $SITE;
370
371 // recreate globals
372 $OUTPUT = new bootstrap_renderer();
373 $PAGE = new moodle_page();
374 $FULLME = null;
375 $ME = null;
376 $SCRIPT = null;
377 $SESSION = new stdClass();
378 $_SESSION['SESSION'] =& $SESSION;
5bd40408 379
a3d5830a 380 // set fresh new user
5bd40408
PS
381 $user = new stdClass();
382 $user->id = 0;
5bd40408
PS
383 $user->mnethostid = $CFG->mnet_localhost_id;
384 session_set_user($user);
a3d5830a
PS
385
386 // reset all static caches
387 accesslib_clear_all_caches(true);
388 get_string_manager()->reset_caches();
389 //TODO: add more resets here and probably refactor them to new core function
390
391 // purge dataroot
728eadac 392 self::reset_dataroot();
a3d5830a
PS
393
394 // restore original config once more in case resetting of caches changes CFG
395 $CFG = self::get_global_backup('CFG');
396
50be93d1
PS
397 // verify db writes just in case something goes wrong in reset
398 if (self::$lastdbwrites != $DB->perf_get_writes()) {
399 error_log('Unexpected DB writes in reset_all_data.');
400 self::$lastdbwrites = $DB->perf_get_writes();
401 }
a3d5830a
PS
402
403 // inform data generator
404 self::get_data_generator()->reset();
405
406 // fix PHP settings
407 error_reporting($CFG->debug);
7b0ff213
PS
408
409 if ($warnings) {
410 $warnings = implode("\n", $warnings);
411 trigger_error($warnings, E_USER_WARNING);
412 }
5bd40408
PS
413 }
414
415 /**
416 * Called during bootstrap only!
417 * @static
5bd40408 418 */
a3d5830a 419 public static function bootstrap_init() {
458b3386 420 global $CFG, $SITE;
5bd40408 421
a3d5830a 422 // backup the globals
6e2cff2d 423 self::$globals['_SERVER'] = $_SERVER;
5bd40408 424 self::$globals['CFG'] = clone($CFG);
458b3386 425 self::$globals['SITE'] = clone($SITE);
a3d5830a
PS
426
427 // refresh data in all tables, clear caches, etc.
428 phpunit_util::reset_all_data();
5bd40408
PS
429 }
430
431 /**
432 * Returns original state of global variable.
433 * @static
434 * @param string $name
435 * @return mixed
436 */
437 public static function get_global_backup($name) {
438 if (isset(self::$globals[$name])) {
439 if (is_object(self::$globals[$name])) {
440 $return = clone(self::$globals[$name]);
441 return $return;
442 } else {
443 return self::$globals[$name];
444 }
445 }
446 return null;
447 }
448
449 /**
450 * Does this site (db and dataroot) appear to be used for production?
451 * We try very hard to prevent accidental damage done to production servers!!
452 *
453 * @static
454 * @return bool
455 */
456 public static function is_test_site() {
457 global $DB, $CFG;
458
459 if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
460 // this is already tested in bootstrap script,
a3d5830a 461 // but anyway presence of this file means the dataroot is for testing
5bd40408
PS
462 return false;
463 }
464
465 $tables = $DB->get_tables(false);
466 if ($tables) {
467 if (!$DB->get_manager()->table_exists('config')) {
468 return false;
469 }
470 if (!get_config('core', 'phpunittest')) {
471 return false;
472 }
473 }
474
475 return true;
476 }
477
478 /**
479 * Is this site initialised to run unit tests?
480 *
481 * @static
7b0ff213 482 * @return int array errorcode=>message, 0 means ok
5bd40408 483 */
a3d5830a 484 public static function testing_ready_problem() {
7b0ff213 485 global $CFG, $DB;
5bd40408 486
7b0ff213 487 $tables = $DB->get_tables(false);
5bd40408 488
7b0ff213
PS
489 if (!self::is_test_site()) {
490 // dataroot was verified in bootstrap, so it must be DB
491 return array(131, 'Can not use test database, try changing prefix');
5bd40408
PS
492 }
493
0b9251e3 494 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
6e2cff2d 495 if (empty($tables)) {
7b0ff213
PS
496 return array(132, '');
497 } else {
498 return array(133, '');
499 }
5bd40408
PS
500 }
501
502 if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
6e2cff2d 503 if (empty($tables)) {
7b0ff213
PS
504 return array(132, '');
505 } else {
506 return array(133, '');
507 }
5bd40408
PS
508 }
509
510 $hash = phpunit_util::get_version_hash();
511 $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
512
513 if ($hash !== $oldhash) {
7b0ff213 514 return array(133, '');
5bd40408
PS
515 }
516
7b0ff213 517 return array(0, '');
5bd40408
PS
518 }
519
520 /**
521 * Drop all test site data.
522 *
523 * Note: To be used from CLI scripts only.
524 *
525 * @static
7aea08e1 526 * @return void may terminate execution with exit code
5bd40408
PS
527 */
528 public static function drop_site() {
529 global $DB, $CFG;
530
531 if (!self::is_test_site()) {
532 cli_error('Can not drop non-test sites!!', 131);
533 }
534
4be2ad36 535 // purge dataroot
728eadac 536 self::reset_dataroot();
5bd40408 537 phpunit_bootstrap_initdataroot($CFG->dataroot);
4be2ad36
PS
538 $keep = array('.', '..', 'lock', 'webrunner.xml');
539 $files = scandir("$CFG->dataroot/phpunit");
540 foreach ($files as $file) {
541 if (in_array($file, $keep)) {
542 continue;
543 }
544 $path = "$CFG->dataroot/phpunit/$file";
545 if (is_dir($path)) {
546 remove_dir($path, false);
547 } else {
548 unlink($path);
549 }
550 }
5bd40408
PS
551
552 // drop all tables
5bd40408 553 $tables = $DB->get_tables(false);
728eadac
PS
554 if (isset($tables['config'])) {
555 // config always last to prevent problems with interrupted drops!
556 unset($tables['config']);
557 $tables['config'] = 'config';
5bd40408 558 }
5bd40408
PS
559 foreach ($tables as $tablename) {
560 $table = new xmldb_table($tablename);
561 $DB->get_manager()->drop_table($table);
562 }
563 }
564
565 /**
566 * Perform a fresh test site installation
567 *
568 * Note: To be used from CLI scripts only.
569 *
570 * @static
7aea08e1 571 * @return void may terminate execution with exit code
5bd40408
PS
572 */
573 public static function install_site() {
574 global $DB, $CFG;
575
576 if (!self::is_test_site()) {
577 cli_error('Can not install non-test sites!!', 131);
578 }
579
580 if ($DB->get_tables()) {
581 cli_error('Database tables already installed, drop the site first.', 133);
582 }
583
584 $options = array();
6e2cff2d 585 $options['adminpass'] = 'admin';
5bd40408
PS
586 $options['shortname'] = 'phpunit';
587 $options['fullname'] = 'PHPUnit test site';
588
589 install_cli_database($options, false);
590
a3d5830a
PS
591 // install timezone info
592 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
593 update_timezone_records($timezones);
5bd40408
PS
594
595 // add test db flag
596 set_config('phpunittest', 'phpunittest');
597
598 // store data for all tables
599 $data = array();
0b9251e3 600 $structure = array();
5bd40408
PS
601 $tables = $DB->get_tables();
602 foreach ($tables as $table) {
728eadac 603 $columns = $DB->get_columns($table);
0b9251e3 604 $structure[$table] = $columns;
714f3998 605 if (isset($columns['id']) and $columns['id']->primary_key) {
728eadac
PS
606 $data[$table] = $DB->get_records($table, array(), 'id ASC');
607 } else {
608 // there should not be many of these
609 $data[$table] = $DB->get_records($table, array());
610 }
5bd40408
PS
611 }
612 $data = serialize($data);
5bd40408 613 file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
6e2cff2d 614 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
5bd40408 615
0b9251e3
PS
616 $structure = serialize($structure);
617 file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
618 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
619
5bd40408
PS
620 // hash all plugin versions - helps with very fast detection of db structure changes
621 $hash = phpunit_util::get_version_hash();
5bd40408 622 file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
6e2cff2d 623 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
5bd40408
PS
624 }
625
626 /**
a3d5830a 627 * Calculate unique version hash for all available plugins and core.
5bd40408
PS
628 * @static
629 * @return string sha1 hash
630 */
631 public static function get_version_hash() {
632 global $CFG;
633
634 $versions = array();
635
636 // main version first
637 $version = null;
638 include($CFG->dirroot.'/version.php');
639 $versions['core'] = $version;
640
641 // modules
642 $mods = get_plugin_list('mod');
643 ksort($mods);
644 foreach ($mods as $mod => $fullmod) {
645 $module = new stdClass();
646 $module->version = null;
647 include($fullmod.'/version.php');
648 $versions[$mod] = $module->version;
649 }
650
651 // now the rest of plugins
652 $plugintypes = get_plugin_types();
653 unset($plugintypes['mod']);
654 ksort($plugintypes);
655 foreach ($plugintypes as $type=>$unused) {
656 $plugs = get_plugin_list($type);
657 ksort($plugs);
658 foreach ($plugs as $plug=>$fullplug) {
659 $plugin = new stdClass();
660 $plugin->version = null;
661 @include($fullplug.'/version.php');
662 $versions[$plug] = $plugin->version;
663 }
664 }
665
666 $hash = sha1(serialize($versions));
667
668 return $hash;
669 }
670
671 /**
7b0ff213 672 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml file using defaults from /phpunit.xml.dist
5bd40408 673 * @static
7b0ff213 674 * @return bool true means main config file created, false means only dataroot file created
5bd40408
PS
675 */
676 public static function build_config_file() {
677 global $CFG;
678
679 $template = '
5bd40408
PS
680 <testsuite name="@component@">
681 <directory suffix="_test.php">@dir@</directory>
a3d5830a 682 </testsuite>';
5bd40408
PS
683 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
684
685 $suites = '';
686
687 $plugintypes = get_plugin_types();
688 ksort($plugintypes);
689 foreach ($plugintypes as $type=>$unused) {
690 $plugs = get_plugin_list($type);
691 ksort($plugs);
692 foreach ($plugs as $plug=>$fullplug) {
693 if (!file_exists("$fullplug/tests/")) {
694 continue;
695 }
696 $dir = preg_replace("|$CFG->dirroot/|", '', $fullplug, 1);
697 $dir .= '/tests';
698 $component = $type.'_'.$plug;
699
700 $suite = str_replace('@component@', $component, $template);
701 $suite = str_replace('@dir@', $dir, $suite);
702
703 $suites .= $suite;
704 }
705 }
706
7aea08e1 707 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
5bd40408 708
7b0ff213
PS
709 $result = false;
710 if (is_writable($CFG->dirroot)) {
711 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
6e2cff2d 712 phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
7b0ff213
PS
713 }
714 }
6e2cff2d 715 // 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
716 $data = str_replace('lib/phpunit/', "$CFG->dirroot/lib/phpunit/", $data);
717 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|', '<directory suffix="_test.php">'.$CFG->dirroot.'/$1</directory>', $data);
718 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
6e2cff2d 719 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
7b0ff213
PS
720
721 return (bool)$result;
5bd40408
PS
722 }
723}
724
725
726/**
727 * Simplified emulation test case for legacy SimpleTest.
728 *
729 * Note: this is supposed to work for very simple tests only.
730 *
7aea08e1
SH
731 * @deprecated since 2.3
732 * @package core
733 * @category phpunit
5bd40408
PS
734 * @author Petr Skoda
735 * @copyright 2012 Petr Skoda {@link http://skodak.org}
736 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
737 */
738class UnitTestCase extends PHPUnit_Framework_TestCase {
739
740 /**
7aea08e1 741 * @deprecated since 2.3
5bd40408
PS
742 * @param bool $expected
743 * @param string $message
a3d5830a 744 * @return void
5bd40408
PS
745 */
746 public function expectException($expected, $message = '') {
747 // use phpdocs: @expectedException ExceptionClassName
748 if (!$expected) {
749 return;
750 }
751 $this->setExpectedException('moodle_exception', $message);
752 }
753
754 /**
7aea08e1 755 * @deprecated since 2.3
5bd40408
PS
756 * @param bool $expected
757 * @param string $message
a3d5830a 758 * @return void
5bd40408
PS
759 */
760 public static function expectError($expected = false, $message = '') {
761 // not available in PHPUnit
762 if (!$expected) {
763 return;
764 }
765 self::skipIf(true);
766 }
767
768 /**
7aea08e1 769 * @deprecated since 2.3
5bd40408
PS
770 * @static
771 * @param mixed $actual
772 * @param string $messages
a3d5830a 773 * @return void
5bd40408
PS
774 */
775 public static function assertTrue($actual, $messages = '') {
776 parent::assertTrue((bool)$actual, $messages);
777 }
778
779 /**
7aea08e1 780 * @deprecated since 2.3
5bd40408
PS
781 * @static
782 * @param mixed $actual
783 * @param string $messages
a3d5830a 784 * @return void
5bd40408
PS
785 */
786 public static function assertFalse($actual, $messages = '') {
787 parent::assertFalse((bool)$actual, $messages);
788 }
789
790 /**
7aea08e1 791 * @deprecated since 2.3
5bd40408
PS
792 * @static
793 * @param mixed $expected
794 * @param mixed $actual
795 * @param string $message
a3d5830a 796 * @return void
5bd40408
PS
797 */
798 public static function assertEqual($expected, $actual, $message = '') {
799 parent::assertEquals($expected, $actual, $message);
800 }
801
802 /**
7aea08e1 803 * @deprecated since 2.3
5bd40408
PS
804 * @static
805 * @param mixed $expected
806 * @param mixed $actual
807 * @param string $message
a3d5830a 808 * @return void
5bd40408
PS
809 */
810 public static function assertNotEqual($expected, $actual, $message = '') {
811 parent::assertNotEquals($expected, $actual, $message);
812 }
813
814 /**
7aea08e1 815 * @deprecated since 2.3
5bd40408
PS
816 * @static
817 * @param mixed $expected
818 * @param mixed $actual
819 * @param string $message
a3d5830a 820 * @return void
5bd40408
PS
821 */
822 public static function assertIdentical($expected, $actual, $message = '') {
823 parent::assertSame($expected, $actual, $message);
824 }
825
826 /**
7aea08e1 827 * @deprecated since 2.3
5bd40408
PS
828 * @static
829 * @param mixed $expected
830 * @param mixed $actual
831 * @param string $message
a3d5830a 832 * @return void
5bd40408
PS
833 */
834 public static function assertNotIdentical($expected, $actual, $message = '') {
835 parent::assertNotSame($expected, $actual, $message);
836 }
837
838 /**
7aea08e1 839 * @deprecated since 2.3
5bd40408
PS
840 * @static
841 * @param mixed $actual
842 * @param mixed $expected
843 * @param string $message
a3d5830a 844 * @return void
5bd40408
PS
845 */
846 public static function assertIsA($actual, $expected, $message = '') {
6e2cff2d
PS
847 if ($expected === 'array') {
848 parent::assertEquals(gettype($actual), 'array', $message);
849 } else {
850 parent::assertInstanceOf($expected, $actual, $message);
851 }
5bd40408
PS
852 }
853}
854
855
856/**
7aea08e1
SH
857 * The simplest PHPUnit test case customised for Moodle
858 *
a3d5830a 859 * It is intended for isolated tests that do not modify database or any globals.
5bd40408 860 *
7aea08e1
SH
861 * @package core
862 * @category phpunit
5bd40408
PS
863 * @copyright 2012 Petr Skoda {@link http://skodak.org}
864 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
865 */
866class basic_testcase extends PHPUnit_Framework_TestCase {
867
868 /**
869 * Constructs a test case with the given name.
870 *
b0e980d7
PS
871 * Note: use setUp() or setUpBeforeClass() in custom test cases.
872 *
7aea08e1
SH
873 * @param string $name
874 * @param array $data
875 * @param string $dataName
5bd40408 876 */
b0e980d7 877 final public function __construct($name = null, array $data = array(), $dataName = '') {
5bd40408
PS
878 parent::__construct($name, $data, $dataName);
879
880 $this->setBackupGlobals(false);
881 $this->setBackupStaticAttributes(false);
882 $this->setRunTestInSeparateProcess(false);
5bd40408
PS
883 }
884
885 /**
a3d5830a 886 * Runs the bare test sequence and log any changes in global state or database.
5bd40408
PS
887 * @return void
888 */
889 public function runBare() {
a3d5830a 890 parent::runBare();
7b0ff213 891 phpunit_util::reset_all_data(true);
a3d5830a
PS
892 }
893}
5bd40408 894
a3d5830a
PS
895
896/**
897 * Advanced PHPUnit test case customised for Moodle.
898 *
899 * @package core
900 * @category phpunit
901 * @copyright 2012 Petr Skoda {@link http://skodak.org}
902 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
903 */
904class advanced_testcase extends PHPUnit_Framework_TestCase {
458b3386 905 /** @var bool automatically reset everything? null means log changes */
714f3998 906 protected $resetAfterTest = null;
a3d5830a 907
50be93d1
PS
908 /** @var moodle_transaction */
909 protected $testdbtransaction = null;
910
a3d5830a
PS
911 /**
912 * Constructs a test case with the given name.
913 *
b0e980d7
PS
914 * Note: use setUp() or setUpBeforeClass() in custom test cases.
915 *
a3d5830a
PS
916 * @param string $name
917 * @param array $data
918 * @param string $dataName
919 */
b0e980d7 920 final public function __construct($name = null, array $data = array(), $dataName = '') {
a3d5830a
PS
921 parent::__construct($name, $data, $dataName);
922
923 $this->setBackupGlobals(false);
924 $this->setBackupStaticAttributes(false);
925 $this->setRunTestInSeparateProcess(false);
926 }
927
928 /**
929 * Runs the bare test sequence.
930 * @return void
931 */
932 public function runBare() {
50be93d1
PS
933 global $DB;
934
935 if ($DB->get_dbfamily() === 'postgres') {
936 // database must allow rollback of DDL, so no mysql here
937 $this->testdbtransaction = $DB->start_delegated_transaction();
938 }
939
5bd40408
PS
940 parent::runBare();
941
50be93d1
PS
942 if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
943 $this->testdbtransaction = null;
944 }
945 $trans = $this->testdbtransaction;
946
a3d5830a 947 if ($this->resetAfterTest === true) {
50be93d1
PS
948 if ($trans) {
949 $DB->force_transaction_rollback();
950 phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
951 }
714f3998 952 phpunit_util::reset_all_data();
a3d5830a 953 } else if ($this->resetAfterTest === false) {
50be93d1
PS
954 if ($trans) {
955 $trans->allow_commit();
956 }
a3d5830a
PS
957 // keep all data untouched for other tests
958 } else {
959 // reset but log what changed
50be93d1
PS
960 if ($trans) {
961 $trans->allow_commit();
962 }
7b0ff213 963 phpunit_util::reset_all_data(true);
a3d5830a 964 }
a3d5830a
PS
965 }
966
967 /**
968 * Reset everything after current test.
969 * @param bool $reset true means reset state back, false means keep all data for the next test,
970 * null means reset state and show warnings if anything changed
971 * @return void
972 */
973 public function resetAfterTest($reset = true) {
974 $this->resetAfterTest = $reset;
975 }
976
977 /**
978 * Cleanup after all tests are executed.
979 *
980 * Note: do not forget to call this if overridden...
981 *
982 * @static
983 * @return void
984 */
985 public static function tearDownAfterClass() {
986 phpunit_util::reset_all_data();
987 }
988
989 /**
990 * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
991 * @static
992 * @return void
993 */
994 public static function resetAllData() {
995 phpunit_util::reset_all_data();
996 }
997
458b3386
PS
998 /**
999 * Set current $USER, reset access cache.
1000 * @static
e72ea4a5 1001 * @param null|int|stdClass $user user record, null means non-logged-in, integer means userid
458b3386
PS
1002 * @return void
1003 */
e72ea4a5
PS
1004 public static function setUser($user = null) {
1005 global $CFG, $DB;
458b3386 1006
e72ea4a5
PS
1007 if (is_object($user)) {
1008 $user = clone($user);
1009 } else if (!$user) {
458b3386
PS
1010 $user = new stdClass();
1011 $user->id = 0;
1012 $user->mnethostid = $CFG->mnet_localhost_id;
1013 } else {
e72ea4a5 1014 $user = $DB->get_record('user', array('id'=>$user));
458b3386
PS
1015 }
1016 unset($user->description);
1017 unset($user->access);
1018
1019 session_set_user($user);
1020 }
1021
a3d5830a
PS
1022 /**
1023 * Get data generator
1024 * @static
1025 * @return phpunit_data_generator
1026 */
1027 public static function getDataGenerator() {
1028 return phpunit_util::get_data_generator();
1029 }
1030
1031 /**
1032 * Recursively visit all the files in the source tree. Calls the callback
1033 * function with the pathname of each file found.
1034 *
6e2cff2d
PS
1035 * @param string $path the folder to start searching from.
1036 * @param string $callback the method of this class to call with the name of each file found.
1037 * @param string $fileregexp a regexp used to filter the search (optional).
1038 * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
a3d5830a
PS
1039 * only files that match the regexp will be included. (default false).
1040 * @param array $ignorefolders will not go into any of these folders (optional).
1041 * @return void
1042 */
1043 public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
1044 $files = scandir($path);
1045
1046 foreach ($files as $file) {
1047 $filepath = $path .'/'. $file;
1048 if (strpos($file, '.') === 0) {
1049 /// Don't check hidden files.
1050 continue;
1051 } else if (is_dir($filepath)) {
1052 if (!in_array($filepath, $ignorefolders)) {
1053 $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
1054 }
1055 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
1056 $this->$callback($filepath);
5bd40408 1057 }
a3d5830a
PS
1058 }
1059 }
1060}
5bd40408 1061
a3d5830a
PS
1062
1063/**
458b3386
PS
1064 * Special test case for testing of DML drivers and DDL layer.
1065 *
1066 * Note: Use only 'test_table*' when creating new tables.
a3d5830a
PS
1067 *
1068 * @package core
1069 * @category phpunit
1070 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1071 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1072 */
1073class database_driver_testcase extends PHPUnit_Framework_TestCase {
6e2cff2d 1074 /** @var moodle_database connection to extra database */
a3d5830a 1075 protected static $extradb = null;
a3d5830a 1076
6e2cff2d 1077 /** @var moodle_database used in these tests*/
a3d5830a
PS
1078 protected $tdb;
1079
1080 /**
1081 * Constructs a test case with the given name.
1082 *
1083 * @param string $name
1084 * @param array $data
1085 * @param string $dataName
1086 */
b0e980d7 1087 final public function __construct($name = null, array $data = array(), $dataName = '') {
a3d5830a
PS
1088 parent::__construct($name, $data, $dataName);
1089
1090 $this->setBackupGlobals(false);
1091 $this->setBackupStaticAttributes(false);
1092 $this->setRunTestInSeparateProcess(false);
1093 }
1094
1095 public static function setUpBeforeClass() {
1096 global $CFG;
6e2cff2d 1097 parent::setUpBeforeClass();
a3d5830a
PS
1098
1099 if (!defined('PHPUNIT_TEST_DRIVER')) {
1100 // use normal $DB
1101 return;
5bd40408 1102 }
a3d5830a
PS
1103
1104 if (!isset($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER])) {
1105 throw new exception('Can not find driver configuration options with index: '.PHPUNIT_TEST_DRIVER);
1106 }
1107
1108 $dblibrary = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']) ? 'native' : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary'];
1109 $dbtype = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbtype'];
1110 $dbhost = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbhost'];
1111 $dbname = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbname'];
1112 $dbuser = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbuser'];
1113 $dbpass = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbpass'];
1114 $prefix = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['prefix'];
1115 $dboptions = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']) ? array() : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions'];
1116
1117 $classname = "{$dbtype}_{$dblibrary}_moodle_database";
1118 require_once("$CFG->libdir/dml/$classname.php");
1119 $d = new $classname();
1120 if (!$d->driver_installed()) {
1121 throw new exception('Database driver for '.$classname.' is not installed');
1122 }
1123
1124 $d->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
1125
1126 self::$extradb = $d;
1127 }
1128
1cbf2a20 1129 protected function setUp() {
a3d5830a 1130 global $DB;
6e2cff2d 1131 parent::setUp();
5bd40408 1132
a3d5830a
PS
1133 if (self::$extradb) {
1134 $this->tdb = self::$extradb;
1135 } else {
1136 $this->tdb = $DB;
5bd40408 1137 }
a3d5830a 1138 }
5bd40408 1139
1cbf2a20 1140 protected function tearDown() {
a3d5830a
PS
1141 // delete all test tables
1142 $dbman = $this->tdb->get_manager();
1143 $tables = $this->tdb->get_tables(false);
1144 foreach($tables as $tablename) {
1145 if (strpos($tablename, 'test_table') === 0) {
1146 $table = new xmldb_table($tablename);
1147 $dbman->drop_table($table);
1148 }
5bd40408 1149 }
6e2cff2d 1150 parent::tearDown();
a3d5830a 1151 }
5bd40408 1152
a3d5830a
PS
1153 public static function tearDownAfterClass() {
1154 if (self::$extradb) {
1155 self::$extradb->dispose();
1156 self::$extradb = null;
1157 }
1158 phpunit_util::reset_all_data();
6e2cff2d 1159 parent::tearDownAfterClass();
5bd40408 1160 }
a3d5830a 1161}