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