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