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