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
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
667c0907
TH
435 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
436 return false;
437 }
438
a3d5830a
PS
439 $tables = $DB->get_tables(false);
440 if (!$tables or empty($tables['config'])) {
441 // not installed yet
fe35ccaf 442 return false;
5bd40408
PS
443 }
444
fe35ccaf
PS
445 if (!$data = self::get_tabledata()) {
446 // not initialised yet
447 return false;
448 }
0b9251e3
PS
449 if (!$structure = self::get_tablestructure()) {
450 // not initialised yet
451 return false;
452 }
728eadac 453
e10736b3
PS
454 $empties = self::guess_unmodified_empty_tables();
455
fe35ccaf
PS
456 foreach ($data as $table=>$records) {
457 if (empty($records)) {
e10736b3
PS
458 if (isset($empties[$table])) {
459 // table was not modified and is empty
460 } else {
461 $DB->delete_records($table, array());
462 }
fe35ccaf
PS
463 continue;
464 }
465
ab483c0a 466 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
714f3998
PS
467 $currentrecords = $DB->get_records($table, array(), 'id ASC');
468 $changed = false;
469 foreach ($records as $id=>$record) {
470 if (!isset($currentrecords[$id])) {
471 $changed = true;
472 break;
473 }
474 if ((array)$record != (array)$currentrecords[$id]) {
475 $changed = true;
476 break;
728eadac 477 }
714f3998
PS
478 unset($currentrecords[$id]);
479 }
480 if (!$changed) {
481 if ($currentrecords) {
482 $lastrecord = end($records);
483 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
484 continue;
485 } else {
486 continue;
a3d5830a 487 }
a3d5830a 488 }
fe35ccaf 489 }
728eadac 490
fe35ccaf 491 $DB->delete_records($table, array());
fe35ccaf
PS
492 foreach ($records as $record) {
493 $DB->import_record($table, $record, false, true);
494 }
495 }
714f3998 496
8b5413cc 497 // reset all next record ids - aka sequences
e10736b3 498 self::reset_all_database_sequences($empties);
728eadac 499
fe35ccaf 500 // remove extra tables
714f3998
PS
501 foreach ($tables as $table) {
502 if (!isset($data[$table])) {
503 $DB->get_manager()->drop_table(new xmldb_table($table));
5bd40408 504 }
5bd40408 505 }
a3d5830a
PS
506
507 self::$lastdbwrites = $DB->perf_get_writes();
508
fe35ccaf 509 return true;
5bd40408
PS
510 }
511
728eadac 512 /**
920f4efe 513 * Purge dataroot directory
728eadac
PS
514 * @static
515 * @return void
516 */
517 public static function reset_dataroot() {
518 global $CFG;
519
520 $handle = opendir($CFG->dataroot);
521 $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
522 while (false !== ($item = readdir($handle))) {
523 if (in_array($item, $skip)) {
524 continue;
525 }
526 if (is_dir("$CFG->dataroot/$item")) {
527 remove_dir("$CFG->dataroot/$item", false);
528 } else {
529 unlink("$CFG->dataroot/$item");
530 }
531 }
532 closedir($handle);
533 make_temp_directory('');
534 make_cache_directory('');
535 make_cache_directory('htmlpurifier');
536 }
537
5bd40408
PS
538 /**
539 * Reset contents of all database tables to initial values, reset caches, etc.
540 *
541 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
542 *
920f4efe 543 * @static
a3d5830a 544 * @param bool $logchanges log changes in global state and database in error log
a3d5830a 545 * @return void
5bd40408 546 */
7b0ff213 547 public static function reset_all_data($logchanges = false) {
458b3386 548 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION;
5bd40408 549
95dcf965
PS
550 // reset global $DB in case somebody mocked it
551 $DB = self::get_global_backup('DB');
552
ce9f3beb
PS
553 if ($DB->is_transaction_started()) {
554 // we can not reset inside transaction
555 $DB->force_transaction_rollback();
556 }
557
fe35ccaf 558 $resetdb = self::reset_database();
7b0ff213 559 $warnings = array();
5bd40408 560
a3d5830a 561 if ($logchanges) {
fe35ccaf 562 if ($resetdb) {
6e2cff2d 563 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
a3d5830a
PS
564 }
565
566 $oldcfg = self::get_global_backup('CFG');
458b3386 567 $oldsite = self::get_global_backup('SITE');
a3d5830a
PS
568 foreach($CFG as $k=>$v) {
569 if (!property_exists($oldcfg, $k)) {
6e2cff2d 570 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
a3d5830a 571 } else if ($oldcfg->$k !== $CFG->$k) {
6e2cff2d 572 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
a3d5830a
PS
573 }
574 unset($oldcfg->$k);
575
576 }
577 if ($oldcfg) {
578 foreach($oldcfg as $k=>$v) {
6e2cff2d 579 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
5bd40408 580 }
5bd40408 581 }
a3d5830a
PS
582
583 if ($USER->id != 0) {
6e2cff2d 584 $warnings[] = 'Warning: unexpected change of $USER';
5bd40408 585 }
458b3386
PS
586
587 if ($COURSE->id != $oldsite->id) {
6e2cff2d 588 $warnings[] = 'Warning: unexpected change of $COURSE';
458b3386 589 }
5bd40408 590 }
5bd40408 591
920f4efe 592 // restore original globals
6e2cff2d 593 $_SERVER = self::get_global_backup('_SERVER');
a3d5830a 594 $CFG = self::get_global_backup('CFG');
458b3386
PS
595 $SITE = self::get_global_backup('SITE');
596 $COURSE = $SITE;
597
920f4efe 598 // reinitialise following globals
458b3386
PS
599 $OUTPUT = new bootstrap_renderer();
600 $PAGE = new moodle_page();
601 $FULLME = null;
602 $ME = null;
603 $SCRIPT = null;
604 $SESSION = new stdClass();
605 $_SESSION['SESSION'] =& $SESSION;
5bd40408 606
920f4efe 607 // set fresh new not-logged-in user
5bd40408
PS
608 $user = new stdClass();
609 $user->id = 0;
5bd40408
PS
610 $user->mnethostid = $CFG->mnet_localhost_id;
611 session_set_user($user);
a3d5830a
PS
612
613 // reset all static caches
614 accesslib_clear_all_caches(true);
615 get_string_manager()->reset_caches();
812013b1 616 events_get_handlers('reset');
bc5c10f6 617 textlib::reset_caches();
a3d5830a
PS
618 //TODO: add more resets here and probably refactor them to new core function
619
920f4efe 620 // purge dataroot directory
728eadac 621 self::reset_dataroot();
a3d5830a 622
920f4efe 623 // restore original config once more in case resetting of caches changed CFG
a3d5830a
PS
624 $CFG = self::get_global_backup('CFG');
625
a3d5830a
PS
626 // inform data generator
627 self::get_data_generator()->reset();
628
629 // fix PHP settings
630 error_reporting($CFG->debug);
7b0ff213 631
8b5413cc
PS
632 // verify db writes just in case something goes wrong in reset
633 if (self::$lastdbwrites != $DB->perf_get_writes()) {
920f4efe 634 error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
8b5413cc
PS
635 self::$lastdbwrites = $DB->perf_get_writes();
636 }
637
7b0ff213
PS
638 if ($warnings) {
639 $warnings = implode("\n", $warnings);
640 trigger_error($warnings, E_USER_WARNING);
641 }
5bd40408
PS
642 }
643
644 /**
645 * Called during bootstrap only!
920f4efe 646 * @internal
5bd40408 647 * @static
920f4efe 648 * @return void
5bd40408 649 */
a3d5830a 650 public static function bootstrap_init() {
95dcf965 651 global $CFG, $SITE, $DB;
5bd40408 652
a3d5830a 653 // backup the globals
6e2cff2d 654 self::$globals['_SERVER'] = $_SERVER;
5bd40408 655 self::$globals['CFG'] = clone($CFG);
458b3386 656 self::$globals['SITE'] = clone($SITE);
95dcf965 657 self::$globals['DB'] = $DB;
a3d5830a
PS
658
659 // refresh data in all tables, clear caches, etc.
660 phpunit_util::reset_all_data();
5bd40408
PS
661 }
662
663 /**
664 * Returns original state of global variable.
665 * @static
666 * @param string $name
667 * @return mixed
668 */
669 public static function get_global_backup($name) {
95dcf965
PS
670 if ($name === 'DB') {
671 // no cloning of database object,
672 // we just need the original reference, not original state
673 return self::$globals['DB'];
674 }
5bd40408
PS
675 if (isset(self::$globals[$name])) {
676 if (is_object(self::$globals[$name])) {
677 $return = clone(self::$globals[$name]);
678 return $return;
679 } else {
680 return self::$globals[$name];
681 }
682 }
683 return null;
684 }
685
686 /**
687 * Does this site (db and dataroot) appear to be used for production?
688 * We try very hard to prevent accidental damage done to production servers!!
689 *
690 * @static
691 * @return bool
692 */
693 public static function is_test_site() {
694 global $DB, $CFG;
695
696 if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
697 // this is already tested in bootstrap script,
a3d5830a 698 // but anyway presence of this file means the dataroot is for testing
5bd40408
PS
699 return false;
700 }
701
702 $tables = $DB->get_tables(false);
703 if ($tables) {
704 if (!$DB->get_manager()->table_exists('config')) {
705 return false;
706 }
707 if (!get_config('core', 'phpunittest')) {
708 return false;
709 }
710 }
711
712 return true;
713 }
714
715 /**
716 * Is this site initialised to run unit tests?
717 *
718 * @static
7b0ff213 719 * @return int array errorcode=>message, 0 means ok
5bd40408 720 */
a3d5830a 721 public static function testing_ready_problem() {
7b0ff213 722 global $CFG, $DB;
5bd40408 723
7b0ff213 724 $tables = $DB->get_tables(false);
5bd40408 725
7b0ff213
PS
726 if (!self::is_test_site()) {
727 // dataroot was verified in bootstrap, so it must be DB
0d8e51a6 728 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
5bd40408
PS
729 }
730
de3d1590 731 if (empty($tables)) {
0d8e51a6 732 return array(PHPUNIT_EXITCODE_INSTALL, '');
de3d1590
PS
733 }
734
0b9251e3 735 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
0d8e51a6 736 return array(PHPUNIT_EXITCODE_REINSTALL, '');
5bd40408
PS
737 }
738
739 if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
0d8e51a6 740 return array(PHPUNIT_EXITCODE_REINSTALL, '');
5bd40408
PS
741 }
742
743 $hash = phpunit_util::get_version_hash();
744 $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
745
746 if ($hash !== $oldhash) {
0d8e51a6 747 return array(PHPUNIT_EXITCODE_REINSTALL, '');
5bd40408
PS
748 }
749
e2b8630b
PS
750 $dbhash = get_config('core', 'phpunittest');
751 if ($hash !== $dbhash) {
0d8e51a6 752 return array(PHPUNIT_EXITCODE_REINSTALL, '');
e2b8630b
PS
753 }
754
7b0ff213 755 return array(0, '');
5bd40408
PS
756 }
757
758 /**
759 * Drop all test site data.
760 *
761 * Note: To be used from CLI scripts only.
762 *
763 * @static
7aea08e1 764 * @return void may terminate execution with exit code
5bd40408
PS
765 */
766 public static function drop_site() {
767 global $DB, $CFG;
768
769 if (!self::is_test_site()) {
0d8e51a6 770 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
5bd40408
PS
771 }
772
4be2ad36 773 // purge dataroot
728eadac 774 self::reset_dataroot();
5bd40408 775 phpunit_bootstrap_initdataroot($CFG->dataroot);
4be2ad36
PS
776 $keep = array('.', '..', 'lock', 'webrunner.xml');
777 $files = scandir("$CFG->dataroot/phpunit");
778 foreach ($files as $file) {
779 if (in_array($file, $keep)) {
780 continue;
781 }
782 $path = "$CFG->dataroot/phpunit/$file";
783 if (is_dir($path)) {
784 remove_dir($path, false);
785 } else {
786 unlink($path);
787 }
788 }
5bd40408
PS
789
790 // drop all tables
5bd40408 791 $tables = $DB->get_tables(false);
728eadac
PS
792 if (isset($tables['config'])) {
793 // config always last to prevent problems with interrupted drops!
794 unset($tables['config']);
795 $tables['config'] = 'config';
5bd40408 796 }
5bd40408
PS
797 foreach ($tables as $tablename) {
798 $table = new xmldb_table($tablename);
799 $DB->get_manager()->drop_table($table);
800 }
801 }
802
803 /**
804 * Perform a fresh test site installation
805 *
806 * Note: To be used from CLI scripts only.
807 *
808 * @static
7aea08e1 809 * @return void may terminate execution with exit code
5bd40408
PS
810 */
811 public static function install_site() {
812 global $DB, $CFG;
813
814 if (!self::is_test_site()) {
0d8e51a6 815 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!');
5bd40408
PS
816 }
817
818 if ($DB->get_tables()) {
219d1a4e
PS
819 list($errorcode, $message) = phpunit_util::testing_ready_problem();
820 if ($errorcode) {
0d8e51a6 821 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
219d1a4e 822 } else {
b6b8a193 823 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
219d1a4e 824 }
5bd40408
PS
825 }
826
827 $options = array();
6e2cff2d 828 $options['adminpass'] = 'admin';
5bd40408
PS
829 $options['shortname'] = 'phpunit';
830 $options['fullname'] = 'PHPUnit test site';
831
832 install_cli_database($options, false);
833
a3d5830a
PS
834 // install timezone info
835 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
836 update_timezone_records($timezones);
5bd40408
PS
837
838 // add test db flag
e2b8630b
PS
839 $hash = phpunit_util::get_version_hash();
840 set_config('phpunittest', $hash);
5bd40408
PS
841
842 // store data for all tables
843 $data = array();
0b9251e3 844 $structure = array();
5bd40408
PS
845 $tables = $DB->get_tables();
846 foreach ($tables as $table) {
728eadac 847 $columns = $DB->get_columns($table);
0b9251e3 848 $structure[$table] = $columns;
ab483c0a 849 if (isset($columns['id']) and $columns['id']->auto_increment) {
728eadac
PS
850 $data[$table] = $DB->get_records($table, array(), 'id ASC');
851 } else {
852 // there should not be many of these
853 $data[$table] = $DB->get_records($table, array());
854 }
5bd40408
PS
855 }
856 $data = serialize($data);
5bd40408 857 file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
6e2cff2d 858 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
5bd40408 859
0b9251e3
PS
860 $structure = serialize($structure);
861 file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
862 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
863
5bd40408 864 // hash all plugin versions - helps with very fast detection of db structure changes
5bd40408 865 file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
6e2cff2d 866 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
5bd40408
PS
867 }
868
869 /**
920f4efe 870 * Calculate unique version hash for all plugins and core.
5bd40408
PS
871 * @static
872 * @return string sha1 hash
873 */
874 public static function get_version_hash() {
875 global $CFG;
876
589376d3
PS
877 if (self::$versionhash) {
878 return self::$versionhash;
879 }
880
5bd40408
PS
881 $versions = array();
882
883 // main version first
884 $version = null;
885 include($CFG->dirroot.'/version.php');
886 $versions['core'] = $version;
887
888 // modules
889 $mods = get_plugin_list('mod');
890 ksort($mods);
891 foreach ($mods as $mod => $fullmod) {
892 $module = new stdClass();
893 $module->version = null;
894 include($fullmod.'/version.php');
895 $versions[$mod] = $module->version;
896 }
897
898 // now the rest of plugins
899 $plugintypes = get_plugin_types();
900 unset($plugintypes['mod']);
901 ksort($plugintypes);
902 foreach ($plugintypes as $type=>$unused) {
903 $plugs = get_plugin_list($type);
904 ksort($plugs);
905 foreach ($plugs as $plug=>$fullplug) {
906 $plugin = new stdClass();
907 $plugin->version = null;
908 @include($fullplug.'/version.php');
909 $versions[$plug] = $plugin->version;
910 }
911 }
912
589376d3 913 self::$versionhash = sha1(serialize($versions));
5bd40408 914
589376d3 915 return self::$versionhash;
5bd40408
PS
916 }
917
918 /**
920f4efe 919 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
5bd40408 920 * @static
7b0ff213 921 * @return bool true means main config file created, false means only dataroot file created
5bd40408
PS
922 */
923 public static function build_config_file() {
924 global $CFG;
925
926 $template = '
5bd40408
PS
927 <testsuite name="@component@">
928 <directory suffix="_test.php">@dir@</directory>
a3d5830a 929 </testsuite>';
5bd40408
PS
930 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
931
932 $suites = '';
933
934 $plugintypes = get_plugin_types();
935 ksort($plugintypes);
936 foreach ($plugintypes as $type=>$unused) {
937 $plugs = get_plugin_list($type);
938 ksort($plugs);
939 foreach ($plugs as $plug=>$fullplug) {
940 if (!file_exists("$fullplug/tests/")) {
941 continue;
942 }
322cf284 943 $dir = substr($fullplug, strlen($CFG->dirroot)+1);
5bd40408
PS
944 $dir .= '/tests';
945 $component = $type.'_'.$plug;
946
947 $suite = str_replace('@component@', $component, $template);
948 $suite = str_replace('@dir@', $dir, $suite);
949
950 $suites .= $suite;
951 }
952 }
953
7aea08e1 954 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
5bd40408 955
7b0ff213
PS
956 $result = false;
957 if (is_writable($CFG->dirroot)) {
958 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
6e2cff2d 959 phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
7b0ff213
PS
960 }
961 }
322cf284 962
6e2cff2d 963 // 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
964 $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data);
965 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|',
966 '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
967 $data);
7b0ff213 968 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
6e2cff2d 969 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
7b0ff213
PS
970
971 return (bool)$result;
5bd40408 972 }
cb481742
EL
973
974 /**
8e5c963e 975 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist
cb481742
EL
976 *
977 * @static
8e5c963e 978 * @return void, stops if can not write files
cb481742 979 */
8e5c963e 980 public static function build_component_config_files() {
cb481742
EL
981 global $CFG;
982
983 $template = '
984 <testsuites>
985 <testsuite name="@component@">
8e5c963e 986 <directory suffix="_test.php">.</directory>
cb481742
EL
987 </testsuite>
988 </testsuites>';
989
990 // Use the upstream file as source for the distributed configurations
991 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
cb481742
EL
992 $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate);
993
994 // Get all the components
995 $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests();
996
997 // Get all the directories having tests
998 $directories = self::get_all_directories_with_tests();
999
1000 // Find any directory not covered by proper components
1001 $remaining = array_diff($directories, $components);
1002
1003 // Add them to the list of components
1004 $components += $remaining;
1005
1006 // Create the corresponding phpunit.xml file for each component
1007 foreach ($components as $cname => $cpath) {
1008 // Calculate the component suite
1009 $ctemplate = $template;
1010 $ctemplate = str_replace('@component@', $cname, $ctemplate);
cb481742
EL
1011
1012 // Apply it to the file template
1013 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate);
1014
8e5c963e
PS
1015 // fix link to schema
1016 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/');
1017 $fcontents = str_replace('lib/phpunit/phpunit.xsd', str_repeat('../', $level).'lib/phpunit/phpunit.xsd', $fcontents);
1018 $fcontents = str_replace('lib/phpunit/bootstrap.php', str_repeat('../', $level).'lib/phpunit/bootstrap.php', $fcontents);
1019
cb481742
EL
1020 // Write the file
1021 $result = false;
1022 if (is_writable($cpath)) {
1023 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
1024 phpunit_boostrap_fix_file_permissions("$cpath/phpunit.xml");
1025 }
1026 }
1027 // Problems writing file, throw error
1028 if (!$result) {
1029 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions");
1030 }
1031 }
cb481742
EL
1032 }
1033
1034 /**
8e5c963e 1035 * Returns all the plugins having PHPUnit tests
cb481742 1036 *
8e5c963e 1037 * @return array all the plugins having PHPUnit tests
cb481742
EL
1038 *
1039 */
1040 private static function get_all_plugins_with_tests() {
1041 $pluginswithtests = array();
1042
1043 $plugintypes = get_plugin_types();
1044 ksort($plugintypes);
1045 foreach ($plugintypes as $type => $unused) {
1046 $plugs = get_plugin_list($type);
1047 ksort($plugs);
1048 foreach ($plugs as $plug => $fullplug) {
1049 // Look for tests recursively
1050 if (self::directory_has_tests($fullplug)) {
1051 $pluginswithtests[$type . '_' . $plug] = $fullplug;
1052 }
1053 }
1054 }
1055 return $pluginswithtests;
1056 }
1057
1058 /**
8e5c963e 1059 * Returns all the subsystems having PHPUnit tests
cb481742
EL
1060 *
1061 * Note we are hacking here the list of subsystems
1062 * to cover some well-known subsystems that are not properly
1063 * returned by the {@link get_core_subsystems()} function.
1064 *
8e5c963e 1065 * @return array all the subsystems having PHPUnit tests
cb481742
EL
1066 */
1067 private static function get_all_subsystems_with_tests() {
1068 global $CFG;
1069
1070 $subsystemswithtests = array();
1071
1072 $subsystems = get_core_subsystems();
1073
1074 // Hack the list a bit to cover some well-known ones
1075 $subsystems['backup'] = 'backup';
1076 $subsystems['db-dml'] = 'lib/dml';
1077 $subsystems['db-ddl'] = 'lib/ddl';
1078
1079 ksort($subsystems);
1080 foreach ($subsystems as $subsys => $relsubsys) {
1081 if ($relsubsys === null) {
1082 continue;
1083 }
1084 $fullsubsys = $CFG->dirroot . '/' . $relsubsys;
1085 if (!is_dir($fullsubsys)) {
1086 continue;
1087 }
1088 // Look for tests recursively
1089 if (self::directory_has_tests($fullsubsys)) {
1090 $subsystemswithtests['core_' . $subsys] = $fullsubsys;
1091 }
1092 }
1093 return $subsystemswithtests;
1094 }
1095
1096 /**
1097 * Returns all the directories having tests
1098 *
1099 * @return array all directories having tests
1100 */
1101 private static function get_all_directories_with_tests() {
1102 global $CFG;
1103
1104 $dirs = array();
1105 $dirite = new RecursiveDirectoryIterator($CFG->dirroot);
1106 $iteite = new RecursiveIteratorIterator($dirite);
62f9a5c4
PS
1107 $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
1108 $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
cb481742
EL
1109 foreach ($regite as $path => $element) {
1110 $key = dirname(dirname($path));
1111 $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_');
1112 $dirs[$key] = $value;
1113 }
1114 ksort($dirs);
1115 return array_flip($dirs);
1116 }
1117
1118 /**
1119 * Returns if a given directory has tests (recursively)
1120 *
1121 * @param $dir string full path to the directory to look for phpunit tests
1122 * @return bool if a given directory has tests (true) or no (false)
1123 */
1124 private static function directory_has_tests($dir) {
1125 if (!is_dir($dir)) {
1126 return false;
1127 }
1128
1129 $dirite = new RecursiveDirectoryIterator($dir);
1130 $iteite = new RecursiveIteratorIterator($dirite);
62f9a5c4
PS
1131 $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
1132 $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
cb481742
EL
1133 $regite->rewind();
1134 if ($regite->valid()) {
1135 return true;
1136 }
1137 return false;
1138 }
5bd40408
PS
1139}
1140
1141
1142/**
1143 * Simplified emulation test case for legacy SimpleTest.
1144 *
1145 * Note: this is supposed to work for very simple tests only.
1146 *
7aea08e1
SH
1147 * @deprecated since 2.3
1148 * @package core
1149 * @category phpunit
5bd40408
PS
1150 * @author Petr Skoda
1151 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1152 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1153 */
d2999716 1154abstract class UnitTestCase extends PHPUnit_Framework_TestCase {
5bd40408
PS
1155
1156 /**
7aea08e1 1157 * @deprecated since 2.3
5bd40408
PS
1158 * @param bool $expected
1159 * @param string $message
a3d5830a 1160 * @return void
5bd40408
PS
1161 */
1162 public function expectException($expected, $message = '') {
920f4efe 1163 // alternatively use phpdocs: @expectedException ExceptionClassName
5bd40408
PS
1164 if (!$expected) {
1165 return;
1166 }
1167 $this->setExpectedException('moodle_exception', $message);
1168 }
1169
1170 /**
7aea08e1 1171 * @deprecated since 2.3
5bd40408
PS
1172 * @param bool $expected
1173 * @param string $message
a3d5830a 1174 * @return void
5bd40408 1175 */
6e9f0fd1
PS
1176 public function expectError($expected = false, $message = '') {
1177 // alternatively use phpdocs: @expectedException PHPUnit_Framework_Error
5bd40408
PS
1178 if (!$expected) {
1179 return;
1180 }
6e9f0fd1 1181 $this->setExpectedException('PHPUnit_Framework_Error', $message);
5bd40408
PS
1182 }
1183
1184 /**
7aea08e1 1185 * @deprecated since 2.3
5bd40408
PS
1186 * @static
1187 * @param mixed $actual
1188 * @param string $messages
a3d5830a 1189 * @return void
5bd40408
PS
1190 */
1191 public static function assertTrue($actual, $messages = '') {
1192 parent::assertTrue((bool)$actual, $messages);
1193 }
1194
1195 /**
7aea08e1 1196 * @deprecated since 2.3
5bd40408
PS
1197 * @static
1198 * @param mixed $actual
1199 * @param string $messages
a3d5830a 1200 * @return void
5bd40408
PS
1201 */
1202 public static function assertFalse($actual, $messages = '') {
1203 parent::assertFalse((bool)$actual, $messages);
1204 }
1205
1206 /**
7aea08e1 1207 * @deprecated since 2.3
5bd40408
PS
1208 * @static
1209 * @param mixed $expected
1210 * @param mixed $actual
1211 * @param string $message
a3d5830a 1212 * @return void
5bd40408
PS
1213 */
1214 public static function assertEqual($expected, $actual, $message = '') {
1215 parent::assertEquals($expected, $actual, $message);
1216 }
1217
6e9f0fd1
PS
1218 /**
1219 * @deprecated since 2.3
1220 * @static
1221 * @param mixed $expected
1222 * @param mixed $actual
1223 * @param float|int $margin
1224 * @param string $message
1225 * @return void
1226 */
1227 public static function assertWithinMargin($expected, $actual, $margin, $message = '') {
1228 parent::assertEquals($expected, $actual, '', $margin, $message);
1229 }
1230
5bd40408 1231 /**
7aea08e1 1232 * @deprecated since 2.3
5bd40408
PS
1233 * @static
1234 * @param mixed $expected
1235 * @param mixed $actual
1236 * @param string $message
a3d5830a 1237 * @return void
5bd40408
PS
1238 */
1239 public static function assertNotEqual($expected, $actual, $message = '') {
1240 parent::assertNotEquals($expected, $actual, $message);
1241 }
1242
1243 /**
7aea08e1 1244 * @deprecated since 2.3
5bd40408
PS
1245 * @static
1246 * @param mixed $expected
1247 * @param mixed $actual
1248 * @param string $message
a3d5830a 1249 * @return void
5bd40408
PS
1250 */
1251 public static function assertIdentical($expected, $actual, $message = '') {
1252 parent::assertSame($expected, $actual, $message);
1253 }
1254
1255 /**
7aea08e1 1256 * @deprecated since 2.3
5bd40408
PS
1257 * @static
1258 * @param mixed $expected
1259 * @param mixed $actual
1260 * @param string $message
a3d5830a 1261 * @return void
5bd40408
PS
1262 */
1263 public static function assertNotIdentical($expected, $actual, $message = '') {
1264 parent::assertNotSame($expected, $actual, $message);
1265 }
1266
1267 /**
7aea08e1 1268 * @deprecated since 2.3
5bd40408
PS
1269 * @static
1270 * @param mixed $actual
1271 * @param mixed $expected
1272 * @param string $message
a3d5830a 1273 * @return void
5bd40408
PS
1274 */
1275 public static function assertIsA($actual, $expected, $message = '') {
6e2cff2d 1276 if ($expected === 'array') {
6e9f0fd1 1277 parent::assertEquals('array', gettype($actual), $message);
6e2cff2d
PS
1278 } else {
1279 parent::assertInstanceOf($expected, $actual, $message);
1280 }
5bd40408 1281 }
6e9f0fd1
PS
1282
1283 /**
1284 * @deprecated since 2.3
1285 * @static
1286 * @param mixed $pattern
1287 * @param mixed $string
1288 * @param string $message
1289 * @return void
1290 */
1291 public static function assertPattern($pattern, $string, $message = '') {
1292 parent::assertRegExp($pattern, $string, $message);
1293 }
1294
1295 /**
1296 * @deprecated since 2.3
1297 * @static
1298 * @param mixed $pattern
1299 * @param mixed $string
1300 * @param string $message
1301 * @return void
1302 */
1303 public static function assertNotPattern($pattern, $string, $message = '') {
1304 parent::assertNotRegExp($pattern, $string, $message);
1305 }
5bd40408
PS
1306}
1307
1308
1309/**
7aea08e1
SH
1310 * The simplest PHPUnit test case customised for Moodle
1311 *
a3d5830a 1312 * It is intended for isolated tests that do not modify database or any globals.
5bd40408 1313 *
7aea08e1
SH
1314 * @package core
1315 * @category phpunit
5bd40408
PS
1316 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1317 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1318 */
d2999716 1319abstract class basic_testcase extends PHPUnit_Framework_TestCase {
5bd40408
PS
1320
1321 /**
1322 * Constructs a test case with the given name.
1323 *
920f4efe 1324 * Note: use setUp() or setUpBeforeClass() in your test cases.
b0e980d7 1325 *
7aea08e1
SH
1326 * @param string $name
1327 * @param array $data
1328 * @param string $dataName
5bd40408 1329 */
b0e980d7 1330 final public function __construct($name = null, array $data = array(), $dataName = '') {
5bd40408
PS
1331 parent::__construct($name, $data, $dataName);
1332
1333 $this->setBackupGlobals(false);
1334 $this->setBackupStaticAttributes(false);
1335 $this->setRunTestInSeparateProcess(false);
5bd40408
PS
1336 }
1337
1338 /**
a3d5830a 1339 * Runs the bare test sequence and log any changes in global state or database.
5bd40408
PS
1340 * @return void
1341 */
39e2e9c4 1342 final public function runBare() {
7620602c
PS
1343 global $DB;
1344
713d2091
PS
1345 try {
1346 parent::runBare();
1347 } catch (Exception $e) {
1348 // cleanup after failed expectation
1349 phpunit_util::reset_all_data();
1350 throw $e;
1351 }
7620602c
PS
1352
1353 if ($DB->is_transaction_started()) {
7620602c
PS
1354 phpunit_util::reset_all_data();
1355 throw new coding_exception('basic_testcase '.$this->getName().' is not supposed to use database transactions!');
1356 }
1357
7b0ff213 1358 phpunit_util::reset_all_data(true);
a3d5830a
PS
1359 }
1360}
5bd40408 1361
a3d5830a
PS
1362
1363/**
1364 * Advanced PHPUnit test case customised for Moodle.
1365 *
1366 * @package core
1367 * @category phpunit
1368 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1369 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1370 */
d2999716 1371abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
458b3386 1372 /** @var bool automatically reset everything? null means log changes */
a0c5affe 1373 private $resetAfterTest;
a3d5830a 1374
50be93d1 1375 /** @var moodle_transaction */
a0c5affe 1376 private $testdbtransaction;
50be93d1 1377
a3d5830a
PS
1378 /**
1379 * Constructs a test case with the given name.
1380 *
920f4efe 1381 * Note: use setUp() or setUpBeforeClass() in your test cases.
b0e980d7 1382 *
a3d5830a
PS
1383 * @param string $name
1384 * @param array $data
1385 * @param string $dataName
1386 */
b0e980d7 1387 final public function __construct($name = null, array $data = array(), $dataName = '') {
a3d5830a
PS
1388 parent::__construct($name, $data, $dataName);
1389
1390 $this->setBackupGlobals(false);
1391 $this->setBackupStaticAttributes(false);
1392 $this->setRunTestInSeparateProcess(false);
1393 }
1394
1395 /**
1396 * Runs the bare test sequence.
1397 * @return void
1398 */
39e2e9c4 1399 final public function runBare() {
50be93d1
PS
1400 global $DB;
1401
8b5413cc
PS
1402 if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
1403 // this happens when previous test does not reset, we can not use transactions
1404 $this->testdbtransaction = null;
1405
5a798e7e 1406 } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
50be93d1
PS
1407 // database must allow rollback of DDL, so no mysql here
1408 $this->testdbtransaction = $DB->start_delegated_transaction();
1409 }
1410
713d2091
PS
1411 try {
1412 parent::runBare();
95dcf965
PS
1413 // set DB reference in case somebody mocked it in test
1414 $DB = phpunit_util::get_global_backup('DB');
713d2091
PS
1415 } catch (Exception $e) {
1416 // cleanup after failed expectation
713d2091
PS
1417 phpunit_util::reset_all_data();
1418 throw $e;
1419 }
5bd40408 1420
50be93d1
PS
1421 if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
1422 $this->testdbtransaction = null;
1423 }
50be93d1 1424
a3d5830a 1425 if ($this->resetAfterTest === true) {
8b5413cc 1426 if ($this->testdbtransaction) {
50be93d1 1427 $DB->force_transaction_rollback();
8b5413cc 1428 phpunit_util::reset_all_database_sequences();
50be93d1
PS
1429 phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
1430 }
714f3998 1431 phpunit_util::reset_all_data();
8b5413cc 1432
a3d5830a 1433 } else if ($this->resetAfterTest === false) {
8b5413cc
PS
1434 if ($this->testdbtransaction) {
1435 $this->testdbtransaction->allow_commit();
50be93d1 1436 }
a3d5830a 1437 // keep all data untouched for other tests
8b5413cc 1438
a3d5830a
PS
1439 } else {
1440 // reset but log what changed
8b5413cc 1441 if ($this->testdbtransaction) {
7620602c
PS
1442 try {
1443 $this->testdbtransaction->allow_commit();
1444 } catch (dml_transaction_exception $e) {
7620602c
PS
1445 phpunit_util::reset_all_data();
1446 throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
1447 }
50be93d1 1448 }
7b0ff213 1449 phpunit_util::reset_all_data(true);
a3d5830a 1450 }
7620602c
PS
1451
1452 // make sure test did not forget to close transaction
1453 if ($DB->is_transaction_started()) {
7620602c
PS
1454 phpunit_util::reset_all_data();
1455 if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
1456 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
1457 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
1458 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
1459 }
1460 }
a3d5830a
PS
1461 }
1462
e396e8ed
PS
1463 /**
1464 * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
1465 *
1466 * @param string $xmlFile
1467 * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet
1468 */
1469 protected function createFlatXMLDataSet($xmlFile) {
1470 return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile);
1471 }
1472
1473 /**
1474 * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
1475 *
1476 * @param string $xmlFile
1477 * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet
1478 */
1479 protected function createXMLDataSet($xmlFile) {
1480 return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile);
1481 }
1482
1483 /**
c691274b 1484 * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
e396e8ed
PS
1485 *
1486 * @param array $files array tablename=>cvsfile
1487 * @param string $delimiter
1488 * @param string $enclosure
1489 * @param string $escape
1490 * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet
1491 */
c691274b 1492 protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
e396e8ed
PS
1493 $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape);
1494 foreach($files as $table=>$file) {
1495 $dataSet->addTable($table, $file);
1496 }
1497 return $dataSet;
1498 }
1499
1500 /**
1501 * Creates new ArrayDataSet from given array
1502 *
1503 * @param array $data array of tables, first row in each table is columns
1504 * @return phpunit_ArrayDataSet
1505 */
1506 protected function createArrayDataSet(array $data) {
1507 return new phpunit_ArrayDataSet($data);
1508 }
1509
1510 /**
1511 * Load date into moodle database tables from standard PHPUnit data set.
1512 *
1513 * Note: it is usually better to use data generators
1514 *
1515 * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset
1516 * @return void
1517 */
1518 protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) {
1519 global $DB;
1520
1521 $structure = phpunit_util::get_tablestructure();
1522
1523 foreach($dataset->getTableNames() as $tablename) {
1524 $table = $dataset->getTable($tablename);
1525 $metadata = $dataset->getTableMetaData($tablename);
1526 $columns = $metadata->getColumns();
1527
1528 $doimport = false;
1529 if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
1530 $doimport = in_array('id', $columns);
1531 }
1532
1533 for($r=0; $r<$table->getRowCount(); $r++) {
1534 $record = $table->getRow($r);
1535 if ($doimport) {
1536 $DB->import_record($tablename, $record);
1537 } else {
1538 $DB->insert_record($tablename, $record);
1539 }
1540 }
1541 if ($doimport) {
1542 $DB->get_manager()->reset_sequence(new xmldb_table($tablename));
1543 }
1544 }
1545 }
1546
8b5413cc
PS
1547 /**
1548 * Call this method from test if you want to make sure that
1549 * the resetting of database is done the slow way without transaction
1550 * rollback.
920f4efe
PS
1551 *
1552 * This is useful especially when testing stuff that is not compatible with transactions.
1553 *
8b5413cc
PS
1554 * @return void
1555 */
1556 public function preventResetByRollback() {
1557 if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
1558 $this->testdbtransaction->allow_commit();
1559 $this->testdbtransaction = null;
1560 }
1561 }
1562
a3d5830a
PS
1563 /**
1564 * Reset everything after current test.
1565 * @param bool $reset true means reset state back, false means keep all data for the next test,
1566 * null means reset state and show warnings if anything changed
1567 * @return void
1568 */
1569 public function resetAfterTest($reset = true) {
1570 $this->resetAfterTest = $reset;
1571 }
1572
1573 /**
1574 * Cleanup after all tests are executed.
1575 *
1576 * Note: do not forget to call this if overridden...
1577 *
1578 * @static
1579 * @return void
1580 */
1581 public static function tearDownAfterClass() {
1582 phpunit_util::reset_all_data();
1583 }
1584
1585 /**
1586 * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
1587 * @static
1588 * @return void
1589 */
1590 public static function resetAllData() {
1591 phpunit_util::reset_all_data();
1592 }
1593
458b3386
PS
1594 /**
1595 * Set current $USER, reset access cache.
1596 * @static
e72ea4a5 1597 * @param null|int|stdClass $user user record, null means non-logged-in, integer means userid
458b3386
PS
1598 * @return void
1599 */
e72ea4a5
PS
1600 public static function setUser($user = null) {
1601 global $CFG, $DB;
458b3386 1602
e72ea4a5
PS
1603 if (is_object($user)) {
1604 $user = clone($user);
1605 } else if (!$user) {
458b3386
PS
1606 $user = new stdClass();
1607 $user->id = 0;
1608 $user->mnethostid = $CFG->mnet_localhost_id;
1609 } else {
e72ea4a5 1610 $user = $DB->get_record('user', array('id'=>$user));
458b3386
PS
1611 }
1612 unset($user->description);
1613 unset($user->access);
1614
1615 session_set_user($user);
1616 }
1617
a3d5830a
PS
1618 /**
1619 * Get data generator
1620 * @static
1621 * @return phpunit_data_generator
1622 */
1623 public static function getDataGenerator() {
1624 return phpunit_util::get_data_generator();
1625 }
1626
1627 /**
1628 * Recursively visit all the files in the source tree. Calls the callback
1629 * function with the pathname of each file found.
1630 *
6e2cff2d
PS
1631 * @param string $path the folder to start searching from.
1632 * @param string $callback the method of this class to call with the name of each file found.
1633 * @param string $fileregexp a regexp used to filter the search (optional).
1634 * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
a3d5830a
PS
1635 * only files that match the regexp will be included. (default false).
1636 * @param array $ignorefolders will not go into any of these folders (optional).
1637 * @return void
1638 */
1639 public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
1640 $files = scandir($path);
1641
1642 foreach ($files as $file) {
1643 $filepath = $path .'/'. $file;
1644 if (strpos($file, '.') === 0) {
1645 /// Don't check hidden files.
1646 continue;
1647 } else if (is_dir($filepath)) {
1648 if (!in_array($filepath, $ignorefolders)) {
1649 $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
1650 }
1651 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
1652 $this->$callback($filepath);
5bd40408 1653 }
a3d5830a
PS
1654 }
1655 }
1656}
5bd40408 1657
a3d5830a 1658
e396e8ed
PS
1659/**
1660 * based on array iterator code from PHPUnit documentation by Sebastian Bergmann
1661 * and added new constructor parameter for different array types.
1662 */
1663class phpunit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet {
1664 /**
1665 * @var array
1666 */
1667 protected $tables = array();
1668
1669 /**
1670 * @param array $data
1671 */
1672 public function __construct(array $data) {
1673 foreach ($data AS $tableName => $rows) {
1674 $firstrow = reset($rows);
1675
1676 if (array_key_exists(0, $firstrow)) {
1677 // columns in first row
1678 $columnsInFirstRow = true;
1679 $columns = $firstrow;
1680 $key = key($rows);
1681 unset($rows[$key]);
1682 } else {
1683 // column name is in each row as key
1684 $columnsInFirstRow = false;
1685 $columns = array_keys($firstrow);
1686 }
1687
1688 $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
1689 $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
1690
1691 foreach ($rows AS $row) {
1692 if ($columnsInFirstRow) {
1693 $row = array_combine($columns, $row);
1694 }
1695 $table->addRow($row);
1696 }
1697 $this->tables[$tableName] = $table;
1698 }
1699 }
1700
1701 protected function createIterator($reverse = FALSE) {
1702 return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
1703 }
1704
1705 public function getTable($tableName) {
1706 if (!isset($this->tables[$tableName])) {
1707 throw new InvalidArgumentException("$tableName is not a table in the current database.");
1708 }
1709
1710 return $this->tables[$tableName];
1711 }
1712}
1713
1714
a3d5830a 1715/**
458b3386
PS
1716 * Special test case for testing of DML drivers and DDL layer.
1717 *
c06563c8
PS
1718 * Note: Use only 'test_table*' names when creating new tables.
1719 *
1720 * For DML/DDL developers: you can add following settings to config.php if you want to test different driver than the main one,
1721 * the reason is to allow testing of incomplete drivers that do not allow full PHPUnit environment
1722 * initialisation (the database can be empty).
1723 * $CFG->phpunit_extra_drivers = array(
1724 * 1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'),
1725 * 2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'),
1726 * 3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'),
1727 * 4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'),
1728 * );
1729 * define('PHPUNIT_TEST_DRIVER')=1; //number is index in the previous array
a3d5830a
PS
1730 *
1731 * @package core
1732 * @category phpunit
1733 * @copyright 2012 Petr Skoda {@link http://skodak.org}
1734 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1735 */
d2999716 1736abstract class database_driver_testcase extends PHPUnit_Framework_TestCase {
6e2cff2d 1737 /** @var moodle_database connection to extra database */
a0c5affe 1738 private static $extradb = null;
a3d5830a 1739
6e2cff2d 1740 /** @var moodle_database used in these tests*/
a3d5830a
PS
1741 protected $tdb;
1742
1743 /**
1744 * Constructs a test case with the given name.
1745 *
1746 * @param string $name
1747 * @param array $data
1748 * @param string $dataName
1749 */
b0e980d7 1750 final public function __construct($name = null, array $data = array(), $dataName = '') {
a3d5830a
PS
1751 parent::__construct($name, $data, $dataName);
1752
1753 $this->setBackupGlobals(false);
1754 $this->setBackupStaticAttributes(false);
1755 $this->setRunTestInSeparateProcess(false);
1756 }
1757
1758 public static function setUpBeforeClass() {
1759 global $CFG;
6e2cff2d 1760 parent::setUpBeforeClass();
a3d5830a
PS
1761
1762 if (!defined('PHPUNIT_TEST_DRIVER')) {
1763 // use normal $DB
1764 return;
5bd40408 1765 }
a3d5830a
PS
1766
1767 if (!isset($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER])) {
1768 throw new exception('Can not find driver configuration options with index: '.PHPUNIT_TEST_DRIVER);
1769 }
1770
1771 $dblibrary = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']) ? 'native' : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary'];
1772 $dbtype = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbtype'];
1773 $dbhost = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbhost'];
1774 $dbname = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbname'];
1775 $dbuser = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbuser'];
1776 $dbpass = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbpass'];
1777 $prefix = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['prefix'];
1778 $dboptions = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']) ? array() : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions'];
1779
1780 $classname = "{$dbtype}_{$dblibrary}_moodle_database";
1781 require_once("$CFG->libdir/dml/$classname.php");
1782 $d = new $classname();
1783 if (!$d->driver_installed()) {
1784 throw new exception('Database driver for '.$classname.' is not installed');
1785 }
1786
1787 $d->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
1788
1789 self::$extradb = $d;
1790 }
1791
1cbf2a20 1792 protected function setUp() {
a3d5830a 1793 global $DB;
6e2cff2d 1794 parent::setUp();
5bd40408 1795
a3d5830a
PS
1796 if (self::$extradb) {
1797 $this->tdb = self::$extradb;
1798 } else {
1799 $this->tdb = $DB;
5bd40408 1800 }
a3d5830a 1801 }
5bd40408 1802
1cbf2a20 1803 protected function tearDown() {
a3d5830a
PS
1804 // delete all test tables
1805 $dbman = $this->tdb->get_manager();
1806 $tables = $this->tdb->get_tables(false);
1807 foreach($tables as $tablename) {
1808 if (strpos($tablename, 'test_table') === 0) {
1809 $table = new xmldb_table($tablename);
1810 $dbman->drop_table($table);
1811 }
5bd40408 1812 }
6e2cff2d 1813 parent::tearDown();
a3d5830a 1814 }
5bd40408 1815
a3d5830a
PS
1816 public static function tearDownAfterClass() {
1817 if (self::$extradb) {
1818 self::$extradb->dispose();
1819 self::$extradb = null;
1820 }
1821 phpunit_util::reset_all_data();
6e2cff2d 1822 parent::tearDownAfterClass();
5bd40408 1823 }
a3d5830a 1824}