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