Merge branch 'MDL-35858-master' of git://github.com/FMCorz/moodle
[moodle.git] / lib / phpunit / classes / util.php
CommitLineData
7e7cfe7a
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 * Utility class.
19 *
20 * @package core
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
26
27/**
28 * Collection of utility methods.
29 *
30 * @package core
31 * @category phpunit
32 * @copyright 2012 Petr Skoda {@link http://skodak.org}
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35class phpunit_util {
36 /** @var string current version hash from php files */
37 protected static $versionhash = null;
38
39 /** @var array original content of all database tables*/
40 protected static $tabledata = null;
41
42 /** @var array original structure of all database tables */
43 protected static $tablestructure = null;
44
45 /** @var array original structure of all database tables */
46 protected static $sequencenames = null;
47
48 /** @var array An array of original globals, restored after each test */
49 protected static $globals = array();
50
51 /** @var int last value of db writes counter, used for db resetting */
52 public static $lastdbwrites = null;
53
54 /** @var phpunit_data_generator */
55 protected static $generator = null;
56
57 /** @var resource used for prevention of parallel test execution */
58 protected static $lockhandle = null;
59
ef5b5e05
PS
60 /** @var array list of debugging messages triggered during the last test execution */
61 protected static $debuggings = array();
62
7e7cfe7a
PS
63 /**
64 * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
65 *
66 * Note: do not call manually!
67 *
68 * @internal
69 * @static
70 * @return void
71 */
72 public static function acquire_test_lock() {
73 global $CFG;
74 if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
75 // dataroot not initialised yet
76 return;
77 }
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);
85 if (!$locked) {
86 if ($wouldblock) {
87 echo "Waiting for other test execution to complete...\n";
88 }
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!
101 * @internal
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
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
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
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
158 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
159 // not initialised yet
160 return array();
161 }
162
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)) {
169 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
170 }
171
172 return self::$tabledata;
173 }
174
175 /**
176 * Returns structure of all tables right after installation.
177 * @static
178 * @return array $table=>$records
179 */
180 public static function get_tablestructure() {
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
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
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
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
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);
286 $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
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
295 } else {
296 return array();
297 }
298 }
299
300 /**
301 * Reset all database sequences to initial values.
302 *
303 * @static
304 * @param array $empties tables that are known to be unmodified and empty
305 * @return void
306 */
307 public static function reset_all_database_sequences(array $empties = null) {
308 global $DB;
309
310 if (!$data = self::get_tabledata()) {
311 // not initialised yet
312 return;
313 }
314 if (!$structure = self::get_tablestructure()) {
315 // not initialised yet
316 return;
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) {
324 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
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 }
337
338 } else if ($dbfamily === 'mysql') {
339 $sequences = array();
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)) {
349 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
350 $sequences[$table] = $info->auto_increment;
351 }
352 }
353 $rs->close();
354 $prefix = $DB->get_prefix();
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)) {
359 $nextid = 1;
360 } else {
361 $lastrecord = end($records);
362 $nextid = $lastrecord->id + 1;
363 }
364 if ($sequences[$table] != $nextid) {
365 $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
366 }
367
368 } else {
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);
382 $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
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])) {
399 $DB->get_manager()->reset_sequence($table);
400 } else if ($nextid == $current[$table]) {
401 continue;
402 }
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");
408 }
409 }
410
411 } else {
412 // note: does mssql support any kind of faster reset?
413 if (is_null($empties)) {
414 $empties = self::guess_unmodified_empty_tables();
415 }
416 foreach ($data as $table=>$records) {
417 if (isset($empties[$table])) {
418 continue;
419 }
420 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
421 $DB->get_manager()->reset_sequence($table);
422 }
423 }
424 }
425 }
426
427 /**
428 * Reset all database tables to default values.
429 * @static
430 * @return bool true if reset done, false if skipped
431 */
432 public static function reset_database() {
433 global $DB;
434
435 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
436 return false;
437 }
438
439 $tables = $DB->get_tables(false);
440 if (!$tables or empty($tables['config'])) {
441 // not installed yet
442 return false;
443 }
444
445 if (!$data = self::get_tabledata()) {
446 // not initialised yet
447 return false;
448 }
449 if (!$structure = self::get_tablestructure()) {
450 // not initialised yet
451 return false;
452 }
453
454 $empties = self::guess_unmodified_empty_tables();
455
456 foreach ($data as $table=>$records) {
457 if (empty($records)) {
458 if (isset($empties[$table])) {
459 // table was not modified and is empty
460 } else {
461 $DB->delete_records($table, array());
462 }
463 continue;
464 }
465
466 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
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;
477 }
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;
487 }
488 }
489 }
490
491 $DB->delete_records($table, array());
492 foreach ($records as $record) {
493 $DB->import_record($table, $record, false, true);
494 }
495 }
496
497 // reset all next record ids - aka sequences
498 self::reset_all_database_sequences($empties);
499
500 // remove extra tables
501 foreach ($tables as $table) {
502 if (!isset($data[$table])) {
503 $DB->get_manager()->drop_table(new xmldb_table($table));
504 }
505 }
506
507 self::$lastdbwrites = $DB->perf_get_writes();
508
509 return true;
510 }
511
512 /**
513 * Purge dataroot directory
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
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 *
543 * @static
544 * @param bool $logchanges log changes in global state and database in error log
545 * @return void
546 */
547 public static function reset_all_data($logchanges = false) {
dd420aba 548 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE;
7e7cfe7a 549
d03d5113
PS
550 // Release memory and indirectly call destroy() methods to release resource handles, etc.
551 gc_collect_cycles();
552
ef5b5e05
PS
553 // Show any unhandled debugging messages, the runbare() could already reset it.
554 self::display_debugging_messages();
555 self::reset_debugging();
556
7e7cfe7a
PS
557 // reset global $DB in case somebody mocked it
558 $DB = self::get_global_backup('DB');
559
560 if ($DB->is_transaction_started()) {
561 // we can not reset inside transaction
562 $DB->force_transaction_rollback();
563 }
564
565 $resetdb = self::reset_database();
566 $warnings = array();
567
568 if ($logchanges) {
569 if ($resetdb) {
570 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
571 }
572
573 $oldcfg = self::get_global_backup('CFG');
574 $oldsite = self::get_global_backup('SITE');
575 foreach($CFG as $k=>$v) {
576 if (!property_exists($oldcfg, $k)) {
577 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
578 } else if ($oldcfg->$k !== $CFG->$k) {
579 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
580 }
581 unset($oldcfg->$k);
582
583 }
584 if ($oldcfg) {
585 foreach($oldcfg as $k=>$v) {
586 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
587 }
588 }
589
590 if ($USER->id != 0) {
591 $warnings[] = 'Warning: unexpected change of $USER';
592 }
593
594 if ($COURSE->id != $oldsite->id) {
595 $warnings[] = 'Warning: unexpected change of $COURSE';
596 }
597 }
598
599 // restore original globals
600 $_SERVER = self::get_global_backup('_SERVER');
601 $CFG = self::get_global_backup('CFG');
602 $SITE = self::get_global_backup('SITE');
603 $COURSE = $SITE;
604
605 // reinitialise following globals
606 $OUTPUT = new bootstrap_renderer();
607 $PAGE = new moodle_page();
608 $FULLME = null;
609 $ME = null;
610 $SCRIPT = null;
611 $SESSION = new stdClass();
612 $_SESSION['SESSION'] =& $SESSION;
613
614 // set fresh new not-logged-in user
615 $user = new stdClass();
616 $user->id = 0;
617 $user->mnethostid = $CFG->mnet_localhost_id;
618 session_set_user($user);
619
620 // reset all static caches
621 accesslib_clear_all_caches(true);
a46e11b5
PS
622 get_string_manager()->reset_caches(true);
623 reset_text_filters_cache(true);
7e7cfe7a
PS
624 events_get_handlers('reset');
625 textlib::reset_caches();
6fd1cf05
MG
626 if (class_exists('repository')) {
627 repository::reset_caches();
628 }
dd420aba 629 $GROUPLIB_CACHE = null;
6fd1cf05 630 //TODO MDL-25290: add more resets here and probably refactor them to new core function
7e7cfe7a 631
2beba297 632 // Reset course and module caches.
13d5c938 633 if (class_exists('format_base')) {
2beba297 634 // If file containing class is not loaded, there is no cache there anyway.
13d5c938
PS
635 format_base::reset_course_cache(0);
636 }
637 $reset = 'reset';
638 get_fast_modinfo($reset);
639
7e7cfe7a
PS
640 // purge dataroot directory
641 self::reset_dataroot();
642
643 // restore original config once more in case resetting of caches changed CFG
644 $CFG = self::get_global_backup('CFG');
645
646 // inform data generator
647 self::get_data_generator()->reset();
648
649 // fix PHP settings
650 error_reporting($CFG->debug);
651
652 // verify db writes just in case something goes wrong in reset
653 if (self::$lastdbwrites != $DB->perf_get_writes()) {
654 error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
655 self::$lastdbwrites = $DB->perf_get_writes();
656 }
657
658 if ($warnings) {
659 $warnings = implode("\n", $warnings);
660 trigger_error($warnings, E_USER_WARNING);
661 }
662 }
663
664 /**
665 * Called during bootstrap only!
666 * @internal
667 * @static
668 * @return void
669 */
670 public static function bootstrap_init() {
671 global $CFG, $SITE, $DB;
672
673 // backup the globals
674 self::$globals['_SERVER'] = $_SERVER;
675 self::$globals['CFG'] = clone($CFG);
676 self::$globals['SITE'] = clone($SITE);
677 self::$globals['DB'] = $DB;
678
679 // refresh data in all tables, clear caches, etc.
680 phpunit_util::reset_all_data();
681 }
682
683 /**
684 * Returns original state of global variable.
685 * @static
686 * @param string $name
687 * @return mixed
688 */
689 public static function get_global_backup($name) {
690 if ($name === 'DB') {
691 // no cloning of database object,
692 // we just need the original reference, not original state
693 return self::$globals['DB'];
694 }
695 if (isset(self::$globals[$name])) {
696 if (is_object(self::$globals[$name])) {
697 $return = clone(self::$globals[$name]);
698 return $return;
699 } else {
700 return self::$globals[$name];
701 }
702 }
703 return null;
704 }
705
706 /**
707 * Does this site (db and dataroot) appear to be used for production?
708 * We try very hard to prevent accidental damage done to production servers!!
709 *
710 * @static
711 * @return bool
712 */
713 public static function is_test_site() {
714 global $DB, $CFG;
715
716 if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
717 // this is already tested in bootstrap script,
718 // but anyway presence of this file means the dataroot is for testing
719 return false;
720 }
721
722 $tables = $DB->get_tables(false);
723 if ($tables) {
724 if (!$DB->get_manager()->table_exists('config')) {
725 return false;
726 }
727 if (!get_config('core', 'phpunittest')) {
728 return false;
729 }
730 }
731
732 return true;
733 }
734
735 /**
736 * Is this site initialised to run unit tests?
737 *
738 * @static
739 * @return int array errorcode=>message, 0 means ok
740 */
741 public static function testing_ready_problem() {
742 global $CFG, $DB;
743
744 $tables = $DB->get_tables(false);
745
746 if (!self::is_test_site()) {
747 // dataroot was verified in bootstrap, so it must be DB
748 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
749 }
750
751 if (empty($tables)) {
752 return array(PHPUNIT_EXITCODE_INSTALL, '');
753 }
754
755 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
756 return array(PHPUNIT_EXITCODE_REINSTALL, '');
757 }
758
759 if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
760 return array(PHPUNIT_EXITCODE_REINSTALL, '');
761 }
762
763 $hash = phpunit_util::get_version_hash();
764 $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
765
766 if ($hash !== $oldhash) {
767 return array(PHPUNIT_EXITCODE_REINSTALL, '');
768 }
769
770 $dbhash = get_config('core', 'phpunittest');
771 if ($hash !== $dbhash) {
772 return array(PHPUNIT_EXITCODE_REINSTALL, '');
773 }
774
775 return array(0, '');
776 }
777
778 /**
779 * Drop all test site data.
780 *
781 * Note: To be used from CLI scripts only.
782 *
783 * @static
85b72a75 784 * @param bool $displayprogress if true, this method will echo progress information.
7e7cfe7a
PS
785 * @return void may terminate execution with exit code
786 */
85b72a75 787 public static function drop_site($displayprogress = false) {
7e7cfe7a
PS
788 global $DB, $CFG;
789
790 if (!self::is_test_site()) {
791 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
792 }
793
85b72a75
TH
794 // Purge dataroot
795 if ($displayprogress) {
796 echo "Purging dataroot:\n";
797 }
7e7cfe7a
PS
798 self::reset_dataroot();
799 phpunit_bootstrap_initdataroot($CFG->dataroot);
800 $keep = array('.', '..', 'lock', 'webrunner.xml');
801 $files = scandir("$CFG->dataroot/phpunit");
802 foreach ($files as $file) {
803 if (in_array($file, $keep)) {
804 continue;
805 }
806 $path = "$CFG->dataroot/phpunit/$file";
807 if (is_dir($path)) {
808 remove_dir($path, false);
809 } else {
810 unlink($path);
811 }
812 }
813
814 // drop all tables
815 $tables = $DB->get_tables(false);
816 if (isset($tables['config'])) {
817 // config always last to prevent problems with interrupted drops!
818 unset($tables['config']);
819 $tables['config'] = 'config';
820 }
85b72a75
TH
821
822 if ($displayprogress) {
823 echo "Dropping tables:\n";
824 }
825 $dotsonline = 0;
7e7cfe7a
PS
826 foreach ($tables as $tablename) {
827 $table = new xmldb_table($tablename);
828 $DB->get_manager()->drop_table($table);
85b72a75
TH
829
830 if ($dotsonline == 60) {
831 if ($displayprogress) {
832 echo "\n";
833 }
834 $dotsonline = 0;
835 }
836 if ($displayprogress) {
837 echo '.';
838 }
839 $dotsonline += 1;
840 }
841 if ($displayprogress) {
842 echo "\n";
7e7cfe7a
PS
843 }
844 }
845
846 /**
847 * Perform a fresh test site installation
848 *
849 * Note: To be used from CLI scripts only.
850 *
851 * @static
852 * @return void may terminate execution with exit code
853 */
854 public static function install_site() {
855 global $DB, $CFG;
856
857 if (!self::is_test_site()) {
858 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!');
859 }
860
861 if ($DB->get_tables()) {
862 list($errorcode, $message) = phpunit_util::testing_ready_problem();
863 if ($errorcode) {
864 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
865 } else {
866 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
867 }
868 }
869
870 $options = array();
871 $options['adminpass'] = 'admin';
872 $options['shortname'] = 'phpunit';
873 $options['fullname'] = 'PHPUnit test site';
874
875 install_cli_database($options, false);
876
877 // install timezone info
878 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
879 update_timezone_records($timezones);
880
881 // add test db flag
882 $hash = phpunit_util::get_version_hash();
883 set_config('phpunittest', $hash);
884
885 // store data for all tables
886 $data = array();
887 $structure = array();
888 $tables = $DB->get_tables();
889 foreach ($tables as $table) {
890 $columns = $DB->get_columns($table);
891 $structure[$table] = $columns;
892 if (isset($columns['id']) and $columns['id']->auto_increment) {
893 $data[$table] = $DB->get_records($table, array(), 'id ASC');
894 } else {
895 // there should not be many of these
896 $data[$table] = $DB->get_records($table, array());
897 }
898 }
899 $data = serialize($data);
900 file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
901 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
902
903 $structure = serialize($structure);
904 file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
905 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
906
907 // hash all plugin versions - helps with very fast detection of db structure changes
908 file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
909 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
910 }
911
912 /**
913 * Calculate unique version hash for all plugins and core.
914 * @static
915 * @return string sha1 hash
916 */
917 public static function get_version_hash() {
918 global $CFG;
919
920 if (self::$versionhash) {
921 return self::$versionhash;
922 }
923
924 $versions = array();
925
926 // main version first
927 $version = null;
928 include($CFG->dirroot.'/version.php');
929 $versions['core'] = $version;
930
931 // modules
932 $mods = get_plugin_list('mod');
933 ksort($mods);
934 foreach ($mods as $mod => $fullmod) {
935 $module = new stdClass();
936 $module->version = null;
937 include($fullmod.'/version.php');
938 $versions[$mod] = $module->version;
939 }
940
941 // now the rest of plugins
942 $plugintypes = get_plugin_types();
943 unset($plugintypes['mod']);
944 ksort($plugintypes);
945 foreach ($plugintypes as $type=>$unused) {
946 $plugs = get_plugin_list($type);
947 ksort($plugs);
948 foreach ($plugs as $plug=>$fullplug) {
949 $plugin = new stdClass();
950 $plugin->version = null;
951 @include($fullplug.'/version.php');
952 $versions[$plug] = $plugin->version;
953 }
954 }
955
956 self::$versionhash = sha1(serialize($versions));
957
958 return self::$versionhash;
959 }
960
961 /**
962 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
963 * @static
964 * @return bool true means main config file created, false means only dataroot file created
965 */
966 public static function build_config_file() {
967 global $CFG;
968
969 $template = '
564fcb3b 970 <testsuite name="@component@ test suite">
7e7cfe7a
PS
971 <directory suffix="_test.php">@dir@</directory>
972 </testsuite>';
973 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
974
975 $suites = '';
976
977 $plugintypes = get_plugin_types();
978 ksort($plugintypes);
979 foreach ($plugintypes as $type=>$unused) {
980 $plugs = get_plugin_list($type);
981 ksort($plugs);
982 foreach ($plugs as $plug=>$fullplug) {
983 if (!file_exists("$fullplug/tests/")) {
984 continue;
985 }
986 $dir = substr($fullplug, strlen($CFG->dirroot)+1);
987 $dir .= '/tests';
988 $component = $type.'_'.$plug;
989
990 $suite = str_replace('@component@', $component, $template);
991 $suite = str_replace('@dir@', $dir, $suite);
992
993 $suites .= $suite;
994 }
995 }
996
997 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
998
999 $result = false;
1000 if (is_writable($CFG->dirroot)) {
1001 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
1002 phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
1003 }
1004 }
1005
1006 // 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
1007 $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data);
1008 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|',
1009 '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
1010 $data);
1011 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
1012 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
1013
1014 return (bool)$result;
1015 }
1016
1017 /**
1018 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist
1019 *
1020 * @static
1021 * @return void, stops if can not write files
1022 */
1023 public static function build_component_config_files() {
1024 global $CFG;
1025
1026 $template = '
1027 <testsuites>
1028 <testsuite name="@component@">
1029 <directory suffix="_test.php">.</directory>
1030 </testsuite>
1031 </testsuites>';
1032
1033 // Use the upstream file as source for the distributed configurations
1034 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
1035 $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate);
1036
1037 // Get all the components
1038 $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests();
1039
1040 // Get all the directories having tests
1041 $directories = self::get_all_directories_with_tests();
1042
1043 // Find any directory not covered by proper components
1044 $remaining = array_diff($directories, $components);
1045
1046 // Add them to the list of components
1047 $components += $remaining;
1048
1049 // Create the corresponding phpunit.xml file for each component
1050 foreach ($components as $cname => $cpath) {
1051 // Calculate the component suite
1052 $ctemplate = $template;
1053 $ctemplate = str_replace('@component@', $cname, $ctemplate);
1054
1055 // Apply it to the file template
1056 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate);
1057
1058 // fix link to schema
1059 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/');
ed7259d1 1060 $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents);
7e7cfe7a
PS
1061
1062 // Write the file
1063 $result = false;
1064 if (is_writable($cpath)) {
1065 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
1066 phpunit_boostrap_fix_file_permissions("$cpath/phpunit.xml");
1067 }
1068 }
1069 // Problems writing file, throw error
1070 if (!$result) {
1071 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions");
1072 }
1073 }
1074 }
1075
1076 /**
1077 * Returns all the plugins having PHPUnit tests
1078 *
1079 * @return array all the plugins having PHPUnit tests
1080 *
1081 */
1082 private static function get_all_plugins_with_tests() {
1083 $pluginswithtests = array();
1084
1085 $plugintypes = get_plugin_types();
1086 ksort($plugintypes);
1087 foreach ($plugintypes as $type => $unused) {
1088 $plugs = get_plugin_list($type);
1089 ksort($plugs);
1090 foreach ($plugs as $plug => $fullplug) {
1091 // Look for tests recursively
1092 if (self::directory_has_tests($fullplug)) {
1093 $pluginswithtests[$type . '_' . $plug] = $fullplug;
1094 }
1095 }
1096 }
1097 return $pluginswithtests;
1098 }
1099
1100 /**
1101 * Returns all the subsystems having PHPUnit tests
1102 *
1103 * Note we are hacking here the list of subsystems
1104 * to cover some well-known subsystems that are not properly
1105 * returned by the {@link get_core_subsystems()} function.
1106 *
1107 * @return array all the subsystems having PHPUnit tests
1108 */
1109 private static function get_all_subsystems_with_tests() {
1110 global $CFG;
1111
1112 $subsystemswithtests = array();
1113
1114 $subsystems = get_core_subsystems();
1115
1116 // Hack the list a bit to cover some well-known ones
1117 $subsystems['backup'] = 'backup';
1118 $subsystems['db-dml'] = 'lib/dml';
1119 $subsystems['db-ddl'] = 'lib/ddl';
1120
1121 ksort($subsystems);
1122 foreach ($subsystems as $subsys => $relsubsys) {
1123 if ($relsubsys === null) {
1124 continue;
1125 }
1126 $fullsubsys = $CFG->dirroot . '/' . $relsubsys;
1127 if (!is_dir($fullsubsys)) {
1128 continue;
1129 }
1130 // Look for tests recursively
1131 if (self::directory_has_tests($fullsubsys)) {
1132 $subsystemswithtests['core_' . $subsys] = $fullsubsys;
1133 }
1134 }
1135 return $subsystemswithtests;
1136 }
1137
1138 /**
1139 * Returns all the directories having tests
1140 *
1141 * @return array all directories having tests
1142 */
1143 private static function get_all_directories_with_tests() {
1144 global $CFG;
1145
1146 $dirs = array();
1147 $dirite = new RecursiveDirectoryIterator($CFG->dirroot);
1148 $iteite = new RecursiveIteratorIterator($dirite);
1149 $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
1150 $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
1151 foreach ($regite as $path => $element) {
1152 $key = dirname(dirname($path));
1153 $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_');
1154 $dirs[$key] = $value;
1155 }
1156 ksort($dirs);
1157 return array_flip($dirs);
1158 }
1159
1160 /**
1161 * Returns if a given directory has tests (recursively)
1162 *
1163 * @param $dir string full path to the directory to look for phpunit tests
1164 * @return bool if a given directory has tests (true) or no (false)
1165 */
1166 private static function directory_has_tests($dir) {
1167 if (!is_dir($dir)) {
1168 return false;
1169 }
1170
1171 $dirite = new RecursiveDirectoryIterator($dir);
1172 $iteite = new RecursiveIteratorIterator($dirite);
1173 $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
1174 $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
1175 $regite->rewind();
1176 if ($regite->valid()) {
1177 return true;
1178 }
1179 return false;
1180 }
ef5b5e05
PS
1181
1182 /**
1183 * To be called from debugging() only.
1184 * @param string $message
1185 * @param int $level
1186 * @param string $from
1187 */
1188 public static function debugging_triggered($message, $level, $from) {
1189 // Store only if debugging triggered from actual test,
1190 // we need normal debugging outside of tests to find problems in our phpunit integration.
1191 $backtrace = debug_backtrace();
1192
1193 foreach ($backtrace as $bt) {
1194 $intest = false;
1195 if (isset($bt['object']) and is_object($bt['object'])) {
1196 if ($bt['object'] instanceof PHPUnit_Framework_TestCase) {
1197 if (strpos($bt['function'], 'test') === 0) {
1198 $intest = true;
1199 break;
1200 }
1201 }
1202 }
1203 }
1204 if (!$intest) {
1205 return false;
1206 }
1207
1208 $debug = new stdClass();
1209 $debug->message = $message;
1210 $debug->level = $level;
1211 $debug->from = $from;
1212
1213 self::$debuggings[] = $debug;
1214
1215 return true;
1216 }
1217
1218 /**
1219 * Resets the list of debugging messages.
1220 */
1221 public static function reset_debugging() {
1222 self::$debuggings = array();
1223 }
1224
1225 /**
1226 * Returns all debugging messages triggered during test.
1227 * @return array with instances having message, level and stacktrace property.
1228 */
1229 public static function get_debugging_messages() {
1230 return self::$debuggings;
1231 }
1232
1233 /**
1234 * Prints out any debug messages accumulated during test execution.
1235 * @return bool false if no debug messages, true if debug triggered
1236 */
1237 public static function display_debugging_messages() {
1238 if (empty(self::$debuggings)) {
1239 return false;
1240 }
1241 foreach(self::$debuggings as $debug) {
1242 echo 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n";
1243 }
1244
1245 return true;
1246 }
7e7cfe7a 1247}