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