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