0f95b2f2a5bf6c5de623d56d7ceeac432026e70a
[moodle.git] / lib / phpunit / classes / util.php
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/>.
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  */
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  */
35 class phpunit_util {
36     /** @var string current version hash from php files */
37     protected static $versionhash = null;
39     /** @var array original content of all database tables*/
40     protected static $tabledata = null;
42     /** @var array original structure of all database tables */
43     protected static $tablestructure = null;
45     /** @var array original structure of all database tables */
46     protected static $sequencenames = null;
48     /** @var array An array of original globals, restored after each test */
49     protected static $globals = array();
51     /** @var int last value of db writes counter, used for db resetting */
52     public static $lastdbwrites = null;
54     /** @var phpunit_data_generator */
55     protected static $generator = null;
57     /** @var resource used for prevention of parallel test execution */
58     protected static $lockhandle = null;
60     /** @var array list of debugging messages triggered during the last test execution */
61     protected static $debuggings = array();
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     }
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     }
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     }
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     }
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;
158         if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
159             // not initialised yet
160             return array();
161         }
163         if (!isset(self::$tabledata)) {
164             $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
165             self::$tabledata = unserialize($data);
166         }
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         }
172         return self::$tabledata;
173     }
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;
183         if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
184             // not initialised yet
185             return array();
186         }
188         if (!isset(self::$tablestructure)) {
189             $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
190             self::$tablestructure = unserialize($data);
191         }
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         }
197         return self::$tablestructure;
198     }
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;
208         if (isset(self::$sequencenames)) {
209             return self::$sequencenames;
210         }
212         if (!$structure = self::get_tablestructure()) {
213             return array();
214         }
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         }
224         return self::$sequencenames;
225     }
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;
236         $dbfamily = $DB->get_dbfamily();
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;
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;
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;
295         } else {
296             return array();
297         }
298     }
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;
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         }
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             }
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                         }
368                     } else {
369                         // some problem exists, fallback to standard code
370                         $DB->get_manager()->reset_sequence($table);
371                     }
372                 }
373             }
375         } else if ($dbfamily === 'oracle') {
376             $sequences = phpunit_util::get_sequencenames();
377             $sequences = array_map('strtoupper', $sequences);
378             $lookup = array_flip($sequences);
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();
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             }
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     }
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;
435         if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
436             return false;
437         }
439         $tables = $DB->get_tables(false);
440         if (!$tables or empty($tables['config'])) {
441             // not installed yet
442             return false;
443         }
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         }
454         $empties = self::guess_unmodified_empty_tables();
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             }
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             }
491             $DB->delete_records($table, array());
492             foreach ($records as $record) {
493                 $DB->import_record($table, $record, false, true);
494             }
495         }
497         // reset all next record ids - aka sequences
498         self::reset_all_database_sequences($empties);
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         }
507         self::$lastdbwrites = $DB->perf_get_writes();
509         return true;
510     }
512     /**
513      * Purge dataroot directory
514      * @static
515      * @return void
516      */
517     public static function reset_dataroot() {
518         global $CFG;
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         // Reset the cache API so that it recreates it's required directories as well.
537         cache_factory::reset();
538         // Purge all data from the caches. This is required for consistency.
539         // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
540         // and now we will purge any other caches as well.
541         cache_helper::purge_all();
542     }
544     /**
545      * Reset contents of all database tables to initial values, reset caches, etc.
546      *
547      * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
548      *
549      * @static
550      * @param bool $logchanges log changes in global state and database in error log
551      * @return void
552      */
553     public static function reset_all_data($logchanges = false) {
554         global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE;
556         // Release memory and indirectly call destroy() methods to release resource handles, etc.
557         gc_collect_cycles();
559         // Show any unhandled debugging messages, the runbare() could already reset it.
560         self::display_debugging_messages();
561         self::reset_debugging();
563         // reset global $DB in case somebody mocked it
564         $DB = self::get_global_backup('DB');
566         if ($DB->is_transaction_started()) {
567             // we can not reset inside transaction
568             $DB->force_transaction_rollback();
569         }
571         $resetdb = self::reset_database();
572         $warnings = array();
574         if ($logchanges) {
575             if ($resetdb) {
576                 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
577             }
579             $oldcfg = self::get_global_backup('CFG');
580             $oldsite = self::get_global_backup('SITE');
581             foreach($CFG as $k=>$v) {
582                 if (!property_exists($oldcfg, $k)) {
583                     $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
584                 } else if ($oldcfg->$k !== $CFG->$k) {
585                     $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
586                 }
587                 unset($oldcfg->$k);
589             }
590             if ($oldcfg) {
591                 foreach($oldcfg as $k=>$v) {
592                     $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
593                 }
594             }
596             if ($USER->id != 0) {
597                 $warnings[] = 'Warning: unexpected change of $USER';
598             }
600             if ($COURSE->id != $oldsite->id) {
601                 $warnings[] = 'Warning: unexpected change of $COURSE';
602             }
603         }
605         // restore original globals
606         $_SERVER = self::get_global_backup('_SERVER');
607         $CFG = self::get_global_backup('CFG');
608         $SITE = self::get_global_backup('SITE');
609         $COURSE = $SITE;
611         // reinitialise following globals
612         $OUTPUT = new bootstrap_renderer();
613         $PAGE = new moodle_page();
614         $FULLME = null;
615         $ME = null;
616         $SCRIPT = null;
617         $SESSION = new stdClass();
618         $_SESSION['SESSION'] =& $SESSION;
620         // set fresh new not-logged-in user
621         $user = new stdClass();
622         $user->id = 0;
623         $user->mnethostid = $CFG->mnet_localhost_id;
624         session_set_user($user);
626         // reset all static caches
627         accesslib_clear_all_caches(true);
628         get_string_manager()->reset_caches(true);
629         reset_text_filters_cache(true);
630         events_get_handlers('reset');
631         textlib::reset_caches();
632         if (class_exists('repository')) {
633             repository::reset_caches();
634         }
635         $GROUPLIB_CACHE = null;
636         //TODO MDL-25290: add more resets here and probably refactor them to new core function
638         // Reset course and module caches.
639         if (class_exists('format_base')) {
640             // If file containing class is not loaded, there is no cache there anyway.
641             format_base::reset_course_cache(0);
642         }
643         $reset = 'reset';
644         get_fast_modinfo($reset);
646         // purge dataroot directory
647         self::reset_dataroot();
649         // restore original config once more in case resetting of caches changed CFG
650         $CFG = self::get_global_backup('CFG');
652         // inform data generator
653         self::get_data_generator()->reset();
655         // fix PHP settings
656         error_reporting($CFG->debug);
658         // verify db writes just in case something goes wrong in reset
659         if (self::$lastdbwrites != $DB->perf_get_writes()) {
660             error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
661             self::$lastdbwrites = $DB->perf_get_writes();
662         }
664         if ($warnings) {
665             $warnings = implode("\n", $warnings);
666             trigger_error($warnings, E_USER_WARNING);
667         }
668     }
670     /**
671      * Called during bootstrap only!
672      * @internal
673      * @static
674      * @return void
675      */
676     public static function bootstrap_init() {
677         global $CFG, $SITE, $DB;
679         // backup the globals
680         self::$globals['_SERVER'] = $_SERVER;
681         self::$globals['CFG'] = clone($CFG);
682         self::$globals['SITE'] = clone($SITE);
683         self::$globals['DB'] = $DB;
685         // refresh data in all tables, clear caches, etc.
686         phpunit_util::reset_all_data();
687     }
689     /**
690      * Returns original state of global variable.
691      * @static
692      * @param string $name
693      * @return mixed
694      */
695     public static function get_global_backup($name) {
696         if ($name === 'DB') {
697             // no cloning of database object,
698             // we just need the original reference, not original state
699             return self::$globals['DB'];
700         }
701         if (isset(self::$globals[$name])) {
702             if (is_object(self::$globals[$name])) {
703                 $return = clone(self::$globals[$name]);
704                 return $return;
705             } else {
706                 return self::$globals[$name];
707             }
708         }
709         return null;
710     }
712     /**
713      * Does this site (db and dataroot) appear to be used for production?
714      * We try very hard to prevent accidental damage done to production servers!!
715      *
716      * @static
717      * @return bool
718      */
719     public static function is_test_site() {
720         global $DB, $CFG;
722         if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
723             // this is already tested in bootstrap script,
724             // but anyway presence of this file means the dataroot is for testing
725             return false;
726         }
728         $tables = $DB->get_tables(false);
729         if ($tables) {
730             if (!$DB->get_manager()->table_exists('config')) {
731                 return false;
732             }
733             if (!get_config('core', 'phpunittest')) {
734                 return false;
735             }
736         }
738         return true;
739     }
741     /**
742      * Is this site initialised to run unit tests?
743      *
744      * @static
745      * @return int array errorcode=>message, 0 means ok
746      */
747     public static function testing_ready_problem() {
748         global $CFG, $DB;
750         $tables = $DB->get_tables(false);
752         if (!self::is_test_site()) {
753             // dataroot was verified in bootstrap, so it must be DB
754             return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
755         }
757         if (empty($tables)) {
758             return array(PHPUNIT_EXITCODE_INSTALL, '');
759         }
761         if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
762             return array(PHPUNIT_EXITCODE_REINSTALL, '');
763         }
765         if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
766             return array(PHPUNIT_EXITCODE_REINSTALL, '');
767         }
769         $hash = phpunit_util::get_version_hash();
770         $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
772         if ($hash !== $oldhash) {
773             return array(PHPUNIT_EXITCODE_REINSTALL, '');
774         }
776         $dbhash = get_config('core', 'phpunittest');
777         if ($hash !== $dbhash) {
778             return array(PHPUNIT_EXITCODE_REINSTALL, '');
779         }
781         return array(0, '');
782     }
784     /**
785      * Drop all test site data.
786      *
787      * Note: To be used from CLI scripts only.
788      *
789      * @static
790      * @param bool $displayprogress if true, this method will echo progress information.
791      * @return void may terminate execution with exit code
792      */
793     public static function drop_site($displayprogress = false) {
794         global $DB, $CFG;
796         if (!self::is_test_site()) {
797             phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
798         }
800         // Purge dataroot
801         if ($displayprogress) {
802             echo "Purging dataroot:\n";
803         }
804         self::reset_dataroot();
805         phpunit_bootstrap_initdataroot($CFG->dataroot);
806         $keep = array('.', '..', 'lock', 'webrunner.xml');
807         $files = scandir("$CFG->dataroot/phpunit");
808         foreach ($files as $file) {
809             if (in_array($file, $keep)) {
810                 continue;
811             }
812             $path = "$CFG->dataroot/phpunit/$file";
813             if (is_dir($path)) {
814                 remove_dir($path, false);
815             } else {
816                 unlink($path);
817             }
818         }
820         // drop all tables
821         $tables = $DB->get_tables(false);
822         if (isset($tables['config'])) {
823             // config always last to prevent problems with interrupted drops!
824             unset($tables['config']);
825             $tables['config'] = 'config';
826         }
828         if ($displayprogress) {
829             echo "Dropping tables:\n";
830         }
831         $dotsonline = 0;
832         foreach ($tables as $tablename) {
833             $table = new xmldb_table($tablename);
834             $DB->get_manager()->drop_table($table);
836             if ($dotsonline == 60) {
837                 if ($displayprogress) {
838                     echo "\n";
839                 }
840                 $dotsonline = 0;
841             }
842             if ($displayprogress) {
843                 echo '.';
844             }
845             $dotsonline += 1;
846         }
847         if ($displayprogress) {
848             echo "\n";
849         }
850     }
852     /**
853      * Perform a fresh test site installation
854      *
855      * Note: To be used from CLI scripts only.
856      *
857      * @static
858      * @return void may terminate execution with exit code
859      */
860     public static function install_site() {
861         global $DB, $CFG;
863         if (!self::is_test_site()) {
864             phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!');
865         }
867         if ($DB->get_tables()) {
868             list($errorcode, $message) = phpunit_util::testing_ready_problem();
869             if ($errorcode) {
870                 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
871             } else {
872                 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
873             }
874         }
876         $options = array();
877         $options['adminpass'] = 'admin';
878         $options['shortname'] = 'phpunit';
879         $options['fullname'] = 'PHPUnit test site';
881         install_cli_database($options, false);
883         // install timezone info
884         $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
885         update_timezone_records($timezones);
887         // add test db flag
888         $hash = phpunit_util::get_version_hash();
889         set_config('phpunittest', $hash);
891         // store data for all tables
892         $data = array();
893         $structure = array();
894         $tables = $DB->get_tables();
895         foreach ($tables as $table) {
896             $columns = $DB->get_columns($table);
897             $structure[$table] = $columns;
898             if (isset($columns['id']) and $columns['id']->auto_increment) {
899                 $data[$table] = $DB->get_records($table, array(), 'id ASC');
900             } else {
901                 // there should not be many of these
902                 $data[$table] = $DB->get_records($table, array());
903             }
904         }
905         $data = serialize($data);
906         file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
907         phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
909         $structure = serialize($structure);
910         file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
911         phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
913         // hash all plugin versions - helps with very fast detection of db structure changes
914         file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
915         phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
916     }
918     /**
919      * Calculate unique version hash for all plugins and core.
920      * @static
921      * @return string sha1 hash
922      */
923     public static function get_version_hash() {
924         global $CFG;
926         if (self::$versionhash) {
927             return self::$versionhash;
928         }
930         $versions = array();
932         // main version first
933         $version = null;
934         include($CFG->dirroot.'/version.php');
935         $versions['core'] = $version;
937         // modules
938         $mods = get_plugin_list('mod');
939         ksort($mods);
940         foreach ($mods as $mod => $fullmod) {
941             $module = new stdClass();
942             $module->version = null;
943             include($fullmod.'/version.php');
944             $versions[$mod] = $module->version;
945         }
947         // now the rest of plugins
948         $plugintypes = get_plugin_types();
949         unset($plugintypes['mod']);
950         ksort($plugintypes);
951         foreach ($plugintypes as $type=>$unused) {
952             $plugs = get_plugin_list($type);
953             ksort($plugs);
954             foreach ($plugs as $plug=>$fullplug) {
955                 $plugin = new stdClass();
956                 $plugin->version = null;
957                 @include($fullplug.'/version.php');
958                 $versions[$plug] = $plugin->version;
959             }
960         }
962         self::$versionhash = sha1(serialize($versions));
964         return self::$versionhash;
965     }
967     /**
968      * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
969      * @static
970      * @return bool true means main config file created, false means only dataroot file created
971      */
972     public static function build_config_file() {
973         global $CFG;
975         $template = '
976         <testsuite name="@component@ test suite">
977             <directory suffix="_test.php">@dir@</directory>
978         </testsuite>';
979         $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
981         $suites = '';
983         $plugintypes = get_plugin_types();
984         ksort($plugintypes);
985         foreach ($plugintypes as $type=>$unused) {
986             $plugs = get_plugin_list($type);
987             ksort($plugs);
988             foreach ($plugs as $plug=>$fullplug) {
989                 if (!file_exists("$fullplug/tests/")) {
990                     continue;
991                 }
992                 $dir = substr($fullplug, strlen($CFG->dirroot)+1);
993                 $dir .= '/tests';
994                 $component = $type.'_'.$plug;
996                 $suite = str_replace('@component@', $component, $template);
997                 $suite = str_replace('@dir@', $dir, $suite);
999                 $suites .= $suite;
1000             }
1001         }
1003         $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
1005         $result = false;
1006         if (is_writable($CFG->dirroot)) {
1007             if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
1008                 phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
1009             }
1010         }
1012         // 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
1013         $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data);
1014         $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|',
1015             '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
1016             $data);
1017         file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
1018         phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
1020         return (bool)$result;
1021     }
1023     /**
1024      * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist
1025      *
1026      * @static
1027      * @return void, stops if can not write files
1028      */
1029     public static function build_component_config_files() {
1030         global $CFG;
1032         $template = '
1033         <testsuites>
1034             <testsuite name="@component@">
1035                 <directory suffix="_test.php">.</directory>
1036             </testsuite>
1037         </testsuites>';
1039         // Use the upstream file as source for the distributed configurations
1040         $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
1041         $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate);
1043         // Get all the components
1044         $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests();
1046         // Get all the directories having tests
1047         $directories = self::get_all_directories_with_tests();
1049         // Find any directory not covered by proper components
1050         $remaining = array_diff($directories, $components);
1052         // Add them to the list of components
1053         $components += $remaining;
1055         // Create the corresponding phpunit.xml file for each component
1056         foreach ($components as $cname => $cpath) {
1057             // Calculate the component suite
1058             $ctemplate = $template;
1059             $ctemplate = str_replace('@component@', $cname, $ctemplate);
1061             // Apply it to the file template
1062             $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate);
1064             // fix link to schema
1065             $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/');
1066             $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents);
1068             // Write the file
1069             $result = false;
1070             if (is_writable($cpath)) {
1071                 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
1072                     phpunit_boostrap_fix_file_permissions("$cpath/phpunit.xml");
1073                 }
1074             }
1075             // Problems writing file, throw error
1076             if (!$result) {
1077                 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions");
1078             }
1079         }
1080     }
1082     /**
1083      * Returns all the plugins having PHPUnit tests
1084      *
1085      * @return array all the plugins having PHPUnit tests
1086      *
1087      */
1088     private static function get_all_plugins_with_tests() {
1089         $pluginswithtests = array();
1091         $plugintypes = get_plugin_types();
1092         ksort($plugintypes);
1093         foreach ($plugintypes as $type => $unused) {
1094             $plugs = get_plugin_list($type);
1095             ksort($plugs);
1096             foreach ($plugs as $plug => $fullplug) {
1097                 // Look for tests recursively
1098                 if (self::directory_has_tests($fullplug)) {
1099                     $pluginswithtests[$type . '_' . $plug] = $fullplug;
1100                 }
1101             }
1102         }
1103         return $pluginswithtests;
1104     }
1106     /**
1107      * Returns all the subsystems having PHPUnit tests
1108      *
1109      * Note we are hacking here the list of subsystems
1110      * to cover some well-known subsystems that are not properly
1111      * returned by the {@link get_core_subsystems()} function.
1112      *
1113      * @return array all the subsystems having PHPUnit tests
1114      */
1115     private static function get_all_subsystems_with_tests() {
1116         global $CFG;
1118         $subsystemswithtests = array();
1120         $subsystems = get_core_subsystems();
1122         // Hack the list a bit to cover some well-known ones
1123         $subsystems['backup'] = 'backup';
1124         $subsystems['db-dml'] = 'lib/dml';
1125         $subsystems['db-ddl'] = 'lib/ddl';
1127         ksort($subsystems);
1128         foreach ($subsystems as $subsys => $relsubsys) {
1129             if ($relsubsys === null) {
1130                 continue;
1131             }
1132             $fullsubsys = $CFG->dirroot . '/' . $relsubsys;
1133             if (!is_dir($fullsubsys)) {
1134                 continue;
1135             }
1136             // Look for tests recursively
1137             if (self::directory_has_tests($fullsubsys)) {
1138                 $subsystemswithtests['core_' . $subsys] = $fullsubsys;
1139             }
1140         }
1141         return $subsystemswithtests;
1142     }
1144     /**
1145      * Returns all the directories having tests
1146      *
1147      * @return array all directories having tests
1148      */
1149     private static function get_all_directories_with_tests() {
1150         global $CFG;
1152         $dirs = array();
1153         $dirite = new RecursiveDirectoryIterator($CFG->dirroot);
1154         $iteite = new RecursiveIteratorIterator($dirite);
1155         $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
1156         $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
1157         foreach ($regite as $path => $element) {
1158             $key = dirname(dirname($path));
1159             $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_');
1160             $dirs[$key] = $value;
1161         }
1162         ksort($dirs);
1163         return array_flip($dirs);
1164     }
1166     /**
1167      * Returns if a given directory has tests (recursively)
1168      *
1169      * @param $dir string full path to the directory to look for phpunit tests
1170      * @return bool if a given directory has tests (true) or no (false)
1171      */
1172     private static function directory_has_tests($dir) {
1173         if (!is_dir($dir)) {
1174             return false;
1175         }
1177         $dirite = new RecursiveDirectoryIterator($dir);
1178         $iteite = new RecursiveIteratorIterator($dirite);
1179         $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
1180         $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
1181         $regite->rewind();
1182         if ($regite->valid()) {
1183             return true;
1184         }
1185         return false;
1186     }
1188     /**
1189      * To be called from debugging() only.
1190      * @param string $message
1191      * @param int $level
1192      * @param string $from
1193      */
1194     public static function debugging_triggered($message, $level, $from) {
1195         // Store only if debugging triggered from actual test,
1196         // we need normal debugging outside of tests to find problems in our phpunit integration.
1197         $backtrace = debug_backtrace();
1199         foreach ($backtrace as $bt) {
1200             $intest = false;
1201             if (isset($bt['object']) and is_object($bt['object'])) {
1202                 if ($bt['object'] instanceof PHPUnit_Framework_TestCase) {
1203                     if (strpos($bt['function'], 'test') === 0) {
1204                         $intest = true;
1205                         break;
1206                     }
1207                 }
1208             }
1209         }
1210         if (!$intest) {
1211             return false;
1212         }
1214         $debug = new stdClass();
1215         $debug->message = $message;
1216         $debug->level   = $level;
1217         $debug->from    = $from;
1219         self::$debuggings[] = $debug;
1221         return true;
1222     }
1224     /**
1225      * Resets the list of debugging messages.
1226      */
1227     public static function reset_debugging() {
1228         self::$debuggings = array();
1229     }
1231     /**
1232      * Returns all debugging messages triggered during test.
1233      * @return array with instances having message, level and stacktrace property.
1234      */
1235     public static function get_debugging_messages() {
1236         return self::$debuggings;
1237     }
1239     /**
1240      * Prints out any debug messages accumulated during test execution.
1241      * @return bool false if no debug messages, true if debug triggered
1242      */
1243     public static function display_debugging_messages() {
1244         if (empty(self::$debuggings)) {
1245             return false;
1246         }
1247         foreach(self::$debuggings as $debug) {
1248             echo 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n";
1249         }
1251         return true;
1252     }