cf22fefcc3bb519b87410c723b439dda0b28a414
[moodle.git] / lib / testing / classes / util.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Testing util classes
19  *
20  * @abstract
21  * @package    core
22  * @category   test
23  * @copyright  2012 Petr Skoda {@link http://skodak.org}
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 /**
28  * Utils for test sites creation
29  *
30  * @package   core
31  * @category  test
32  * @copyright 2012 Petr Skoda {@link http://skodak.org}
33  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 abstract class testing_util {
37     /**
38      * @var string dataroot (likely to be $CFG->dataroot).
39      */
40     private static $dataroot = null;
42     /**
43      * @var testing_data_generator
44      */
45     protected static $generator = null;
47     /**
48      * @var string current version hash from php files
49      */
50     protected static $versionhash = null;
52     /**
53      * @var array original content of all database tables
54      */
55     protected static $tabledata = null;
57     /**
58      * @var array original structure of all database tables
59      */
60     protected static $tablestructure = null;
62     /**
63      * @var array keep list of sequenceid used in a table.
64      */
65     private static $tablesequences = array();
67     /**
68      * @var array list of updated tables.
69      */
70     public static $tableupdated = array();
72     /**
73      * @var array original structure of all database tables
74      */
75     protected static $sequencenames = null;
77     /**
78      * @var string name of the json file where we store the list of dataroot files to not reset during reset_dataroot.
79      */
80     private static $originaldatafilesjson = 'originaldatafiles.json';
82     /**
83      * @var boolean set to true once $originaldatafilesjson file is created.
84      */
85     private static $originaldatafilesjsonadded = false;
87     /**
88      * @var int next sequence value for a single test cycle.
89      */
90     protected static $sequencenextstartingid = null;
92     /**
93      * Return the name of the JSON file containing the init filenames.
94      *
95      * @static
96      * @return string
97      */
98     public static function get_originaldatafilesjson() {
99         return self::$originaldatafilesjson;
100     }
102     /**
103      * Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
104      *
105      * @static
106      * @return string the dataroot.
107      */
108     public static function get_dataroot() {
109         global $CFG;
111         //  By default it's the test framework dataroot.
112         if (empty(self::$dataroot)) {
113             self::$dataroot = $CFG->dataroot;
114         }
116         return self::$dataroot;
117     }
119     /**
120      * Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
121      *
122      * @param string $dataroot the dataroot of the test framework.
123      * @static
124      */
125     public static function set_dataroot($dataroot) {
126         self::$dataroot = $dataroot;
127     }
129     /**
130      * Returns the testing framework name
131      * @static
132      * @return string
133      */
134     protected static final function get_framework() {
135         $classname = get_called_class();
136         return substr($classname, 0, strpos($classname, '_'));
137     }
139     /**
140      * Get data generator
141      * @static
142      * @return testing_data_generator
143      */
144     public static function get_data_generator() {
145         if (is_null(self::$generator)) {
146             require_once(__DIR__.'/../generator/lib.php');
147             self::$generator = new testing_data_generator();
148         }
149         return self::$generator;
150     }
152     /**
153      * Does this site (db and dataroot) appear to be used for production?
154      * We try very hard to prevent accidental damage done to production servers!!
155      *
156      * @static
157      * @return bool
158      */
159     public static function is_test_site() {
160         global $DB, $CFG;
162         $framework = self::get_framework();
164         if (!file_exists(self::get_dataroot() . '/' . $framework . 'testdir.txt')) {
165             // this is already tested in bootstrap script,
166             // but anyway presence of this file means the dataroot is for testing
167             return false;
168         }
170         $tables = $DB->get_tables(false);
171         if ($tables) {
172             if (!$DB->get_manager()->table_exists('config')) {
173                 return false;
174             }
175             if (!get_config('core', $framework . 'test')) {
176                 return false;
177             }
178         }
180         return true;
181     }
183     /**
184      * Returns whether test database and dataroot were created using the current version codebase
185      *
186      * @return bool
187      */
188     public static function is_test_data_updated() {
189         global $CFG;
191         $framework = self::get_framework();
193         $datarootpath = self::get_dataroot() . '/' . $framework;
194         if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
195             return false;
196         }
198         if (!file_exists($datarootpath . '/versionshash.txt')) {
199             return false;
200         }
202         $hash = core_component::get_all_versions_hash();
203         $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
205         if ($hash !== $oldhash) {
206             return false;
207         }
209         $dbhash = get_config('core', $framework . 'test');
210         if ($hash !== $dbhash) {
211             return false;
212         }
214         return true;
215     }
217     /**
218      * Stores the status of the database
219      *
220      * Serializes the contents and the structure and
221      * stores it in the test framework space in dataroot
222      */
223     protected static function store_database_state() {
224         global $DB, $CFG;
226         $framework = self::get_framework();
228         // store data for all tables
229         $data = array();
230         $structure = array();
231         $tables = $DB->get_tables();
232         foreach ($tables as $table) {
233             $columns = $DB->get_columns($table);
234             $structure[$table] = $columns;
235             if (isset($columns['id']) and $columns['id']->auto_increment) {
236                 $data[$table] = $DB->get_records($table, array(), 'id ASC');
237             } else {
238                 // there should not be many of these
239                 $data[$table] = $DB->get_records($table, array());
240             }
241         }
242         $data = serialize($data);
243         $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
244         file_put_contents($datafile, $data);
245         testing_fix_file_permissions($datafile);
247         $structure = serialize($structure);
248         $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
249         file_put_contents($structurefile, $structure);
250         testing_fix_file_permissions($structurefile);
251     }
253     /**
254      * Stores the version hash in both database and dataroot
255      */
256     protected static function store_versions_hash() {
257         global $CFG;
259         $framework = self::get_framework();
260         $hash = core_component::get_all_versions_hash();
262         // add test db flag
263         set_config($framework . 'test', $hash);
265         // hash all plugin versions - helps with very fast detection of db structure changes
266         $hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt';
267         file_put_contents($hashfile, $hash);
268         testing_fix_file_permissions($hashfile);
269     }
271     /**
272      * Returns contents of all tables right after installation.
273      * @static
274      * @return array  $table=>$records
275      */
276     protected static function get_tabledata() {
277         if (!isset(self::$tabledata)) {
278             $framework = self::get_framework();
280             $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
281             if (!file_exists($datafile)) {
282                 // Not initialised yet.
283                 return array();
284             }
286             $data = file_get_contents($datafile);
287             self::$tabledata = unserialize($data);
288         }
290         if (!is_array(self::$tabledata)) {
291             testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
292         }
294         return self::$tabledata;
295     }
297     /**
298      * Returns structure of all tables right after installation.
299      * @static
300      * @return array $table=>$records
301      */
302     public static function get_tablestructure() {
303         if (!isset(self::$tablestructure)) {
304             $framework = self::get_framework();
306             $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
307             if (!file_exists($structurefile)) {
308                 // Not initialised yet.
309                 return array();
310             }
312             $data = file_get_contents($structurefile);
313             self::$tablestructure = unserialize($data);
314         }
316         if (!is_array(self::$tablestructure)) {
317             testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
318         }
320         return self::$tablestructure;
321     }
323     /**
324      * Returns the names of sequences for each autoincrementing id field in all standard tables.
325      * @static
326      * @return array $table=>$sequencename
327      */
328     public static function get_sequencenames() {
329         global $DB;
331         if (isset(self::$sequencenames)) {
332             return self::$sequencenames;
333         }
335         if (!$structure = self::get_tablestructure()) {
336             return array();
337         }
339         self::$sequencenames = array();
340         foreach ($structure as $table => $ignored) {
341             $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
342             if ($name !== false) {
343                 self::$sequencenames[$table] = $name;
344             }
345         }
347         return self::$sequencenames;
348     }
350     /**
351      * Returns list of tables that are unmodified and empty.
352      *
353      * @static
354      * @return array of table names, empty if unknown
355      */
356     protected static function guess_unmodified_empty_tables() {
357         global $DB;
359         $dbfamily = $DB->get_dbfamily();
361         if ($dbfamily === 'mysql') {
362             $empties = array();
363             $prefix = $DB->get_prefix();
364             $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
365             foreach ($rs as $info) {
366                 $table = strtolower($info->name);
367                 if (strpos($table, $prefix) !== 0) {
368                     // incorrect table match caused by _
369                     continue;
370                 }
372                 if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
373                     $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
374                     $empties[$table] = $table;
375                 }
376             }
377             $rs->close();
378             return $empties;
380         } else if ($dbfamily === 'mssql') {
381             $empties = array();
382             $prefix = $DB->get_prefix();
383             $sql = "SELECT t.name
384                       FROM sys.identity_columns i
385                       JOIN sys.tables t ON t.object_id = i.object_id
386                      WHERE t.name LIKE ?
387                        AND i.name = 'id'
388                        AND i.last_value IS NULL";
389             $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
390             foreach ($rs as $info) {
391                 $table = strtolower($info->name);
392                 if (strpos($table, $prefix) !== 0) {
393                     // incorrect table match caused by _
394                     continue;
395                 }
396                 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
397                 $empties[$table] = $table;
398             }
399             $rs->close();
400             return $empties;
402         } else if ($dbfamily === 'oracle') {
403             $sequences = self::get_sequencenames();
404             $sequences = array_map('strtoupper', $sequences);
405             $lookup = array_flip($sequences);
406             $empties = array();
407             list($seqs, $params) = $DB->get_in_or_equal($sequences);
408             $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
409             $rs = $DB->get_recordset_sql($sql, $params);
410             foreach ($rs as $seq) {
411                 $table = $lookup[$seq->sequence_name];
412                 $empties[$table] = $table;
413             }
414             $rs->close();
415             return $empties;
417         } else {
418             return array();
419         }
420     }
422     /**
423      * Determine the next unique starting id sequences.
424      *
425      * @static
426      * @param array $records The records to use to determine the starting value for the table.
427      * @param string $table table name.
428      * @return int The value the sequence should be set to.
429      */
430     private static function get_next_sequence_starting_value($records, $table) {
431         if (isset(self::$tablesequences[$table])) {
432             return self::$tablesequences[$table];
433         }
435         $id = self::$sequencenextstartingid;
437         // If there are records, calculate the minimum id we can use.
438         // It must be bigger than the last record's id.
439         if (!empty($records)) {
440             $lastrecord = end($records);
441             $id = max($id, $lastrecord->id + 1);
442         }
444         self::$sequencenextstartingid = $id + 1000;
446         self::$tablesequences[$table] = $id;
448         return $id;
449     }
451     /**
452      * Reset all database sequences to initial values.
453      *
454      * @static
455      * @param array $empties tables that are known to be unmodified and empty
456      * @return void
457      */
458     public static function reset_all_database_sequences(array $empties = null) {
459         global $DB;
461         if (!$data = self::get_tabledata()) {
462             // Not initialised yet.
463             return;
464         }
465         if (!$structure = self::get_tablestructure()) {
466             // Not initialised yet.
467             return;
468         }
470         $updatedtables = self::$tableupdated;
472         // If all starting Id's are the same, it's difficult to detect coding and testing
473         // errors that use the incorrect id in tests.  The classic case is cmid vs instance id.
474         // To reduce the chance of the coding error, we start sequences at different values where possible.
475         // In a attempt to avoid tables with existing id's we start at a high number.
476         // Reset the value each time all database sequences are reset.
477         if (defined('PHPUNIT_SEQUENCE_START') and PHPUNIT_SEQUENCE_START) {
478             self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
479         } else {
480             self::$sequencenextstartingid = 100000;
481         }
483         $dbfamily = $DB->get_dbfamily();
484         if ($dbfamily === 'postgres') {
485             $queries = array();
486             $prefix = $DB->get_prefix();
487             foreach ($data as $table => $records) {
488                 // If table is not modified then no need to do anything.
489                 if (!isset($updatedtables[$table])) {
490                     continue;
491                 }
492                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
493                     $nextid = self::get_next_sequence_starting_value($records, $table);
494                     $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
495                 }
496             }
497             if ($queries) {
498                 $DB->change_database_structure(implode(';', $queries));
499             }
501         } else if ($dbfamily === 'mysql') {
502             $queries = array();
503             $sequences = array();
504             $prefix = $DB->get_prefix();
505             $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
506             foreach ($rs as $info) {
507                 $table = strtolower($info->name);
508                 if (strpos($table, $prefix) !== 0) {
509                     // incorrect table match caused by _
510                     continue;
511                 }
512                 if (!is_null($info->auto_increment)) {
513                     $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
514                     $sequences[$table] = $info->auto_increment;
515                 }
516             }
517             $rs->close();
518             $prefix = $DB->get_prefix();
519             foreach ($data as $table => $records) {
520                 // If table is not modified then no need to do anything.
521                 if (!isset($updatedtables[$table])) {
522                     continue;
523                 }
524                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
525                     if (isset($sequences[$table])) {
526                         $nextid = self::get_next_sequence_starting_value($records, $table);
527                         if ($sequences[$table] != $nextid) {
528                             $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
529                         }
530                     } else {
531                         // some problem exists, fallback to standard code
532                         $DB->get_manager()->reset_sequence($table);
533                     }
534                 }
535             }
536             if ($queries) {
537                 $DB->change_database_structure(implode(';', $queries));
538             }
540         } else if ($dbfamily === 'oracle') {
541             $sequences = self::get_sequencenames();
542             $sequences = array_map('strtoupper', $sequences);
543             $lookup = array_flip($sequences);
545             $current = array();
546             list($seqs, $params) = $DB->get_in_or_equal($sequences);
547             $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
548             $rs = $DB->get_recordset_sql($sql, $params);
549             foreach ($rs as $seq) {
550                 $table = $lookup[$seq->sequence_name];
551                 $current[$table] = $seq->last_number;
552             }
553             $rs->close();
555             foreach ($data as $table => $records) {
556                 // If table is not modified then no need to do anything.
557                 if (!isset($updatedtables[$table])) {
558                     continue;
559                 }
560                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
561                     $lastrecord = end($records);
562                     if ($lastrecord) {
563                         $nextid = $lastrecord->id + 1;
564                     } else {
565                         $nextid = 1;
566                     }
567                     if (!isset($current[$table])) {
568                         $DB->get_manager()->reset_sequence($table);
569                     } else if ($nextid == $current[$table]) {
570                         continue;
571                     }
572                     // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
573                     $seqname = $sequences[$table];
574                     $cachesize = $DB->get_manager()->generator->sequence_cache_size;
575                     $DB->change_database_structure("DROP SEQUENCE $seqname");
576                     $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
577                 }
578             }
580         } else {
581             // note: does mssql support any kind of faster reset?
582             // This also implies mssql will not use unique sequence values.
583             if (is_null($empties) and (empty($updatedtables))) {
584                 $empties = self::guess_unmodified_empty_tables();
585             }
586             foreach ($data as $table => $records) {
587                 // If table is not modified then no need to do anything.
588                 if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
589                     continue;
590                 }
591                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
592                     $DB->get_manager()->reset_sequence($table);
593                 }
594             }
595         }
596     }
598     /**
599      * Reset all database tables to default values.
600      * @static
601      * @return bool true if reset done, false if skipped
602      */
603     public static function reset_database() {
604         global $DB;
606         $tables = $DB->get_tables(false);
607         if (!$tables or empty($tables['config'])) {
608             // not installed yet
609             return false;
610         }
612         if (!$data = self::get_tabledata()) {
613             // not initialised yet
614             return false;
615         }
616         if (!$structure = self::get_tablestructure()) {
617             // not initialised yet
618             return false;
619         }
621         $empties = array();
622         // Use local copy of self::$tableupdated, as list gets updated in for loop.
623         $updatedtables = self::$tableupdated;
625         // If empty tablesequences list then it's the very first run.
626         if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
627             // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
628             $empties = self::guess_unmodified_empty_tables();
629         }
631         // Check if any table has been modified by behat selenium process.
632         if (defined('BEHAT_SITE_RUNNING')) {
633             // Crazy way to reset :(.
634             $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
635             if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
636                 self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
637                 unlink($tablesupdatedfile);
638             }
639             $updatedtables = self::$tableupdated;
640         }
642         $borkedmysql = false;
643         if ($DB->get_dbfamily() === 'mysql') {
644             $version = $DB->get_server_info();
645             if (version_compare($version['version'], '5.6.0') == 1 and version_compare($version['version'], '5.6.16') == -1) {
646                 // Everything that comes from Oracle is evil!
647                 //
648                 // See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
649                 // You cannot reset the counter to a value less than or equal to to the value that is currently in use.
650                 //
651                 // From 5.6.16 release notes:
652                 //   InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
653                 //           (Bug #17250787, Bug #69882)
654                 $borkedmysql = true;
656             } else if (version_compare($version['version'], '10.0.0') == 1) {
657                 // And MariaDB is no better!
658                 // Let's hope they pick the patch sometime later...
659                 $borkedmysql = true;
660             }
661         }
663         if ($borkedmysql) {
664             $mysqlsequences = array();
665             $prefix = $DB->get_prefix();
666             $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
667             foreach ($rs as $info) {
668                 $table = strtolower($info->name);
669                 if (strpos($table, $prefix) !== 0) {
670                     // Incorrect table match caused by _ char.
671                     continue;
672                 }
673                 if (!is_null($info->auto_increment)) {
674                     $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
675                     $mysqlsequences[$table] = $info->auto_increment;
676                 }
677             }
678         }
680         foreach ($data as $table => $records) {
681             // If table is not modified then no need to do anything.
682             // $updatedtables tables is set after the first run, so check before checking for specific table update.
683             if (!empty($updatedtables) && !isset($updatedtables[$table])) {
684                 continue;
685             }
687             if ($borkedmysql) {
688                 if (empty($records)) {
689                     if (!isset($empties[$table])) {
690                         // Table has been modified and is not empty.
691                         $DB->delete_records($table, null);
692                     }
693                     continue;
694                 }
696                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
697                     $current = $DB->get_records($table, array(), 'id ASC');
698                     if ($current == $records) {
699                         if (isset($mysqlsequences[$table]) and $mysqlsequences[$table] == $structure[$table]['id']->auto_increment) {
700                             continue;
701                         }
702                     }
703                 }
705                 // Use TRUNCATE as a workaround and reinsert everything.
706                 $DB->delete_records($table, null);
707                 foreach ($records as $record) {
708                     $DB->import_record($table, $record, false, true);
709                 }
710                 continue;
711             }
713             if (empty($records)) {
714                 if (!isset($empties[$table])) {
715                     // Table has been modified and is not empty.
716                     $DB->delete_records($table, array());
717                 }
718                 continue;
719             }
721             if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
722                 $currentrecords = $DB->get_records($table, array(), 'id ASC');
723                 $changed = false;
724                 foreach ($records as $id => $record) {
725                     if (!isset($currentrecords[$id])) {
726                         $changed = true;
727                         break;
728                     }
729                     if ((array)$record != (array)$currentrecords[$id]) {
730                         $changed = true;
731                         break;
732                     }
733                     unset($currentrecords[$id]);
734                 }
735                 if (!$changed) {
736                     if ($currentrecords) {
737                         $lastrecord = end($records);
738                         $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
739                         continue;
740                     } else {
741                         continue;
742                     }
743                 }
744             }
746             $DB->delete_records($table, array());
747             foreach ($records as $record) {
748                 $DB->import_record($table, $record, false, true);
749             }
750         }
752         // reset all next record ids - aka sequences
753         self::reset_all_database_sequences($empties);
755         // remove extra tables
756         foreach ($tables as $table) {
757             if (!isset($data[$table])) {
758                 $DB->get_manager()->drop_table(new xmldb_table($table));
759             }
760         }
762         self::reset_updated_table_list();
764         return true;
765     }
767     /**
768      * Purge dataroot directory
769      * @static
770      * @return void
771      */
772     public static function reset_dataroot() {
773         global $CFG;
775         $childclassname = self::get_framework() . '_util';
777         // Do not delete automatically installed files.
778         self::skip_original_data_files($childclassname);
780         // Clear file status cache, before checking file_exists.
781         clearstatcache();
783         // Clean up the dataroot folder.
784         $handle = opendir(self::get_dataroot());
785         while (false !== ($item = readdir($handle))) {
786             if (in_array($item, $childclassname::$datarootskiponreset)) {
787                 continue;
788             }
789             if (is_dir(self::get_dataroot()."/$item")) {
790                 remove_dir(self::get_dataroot()."/$item", false);
791             } else {
792                 unlink(self::get_dataroot()."/$item");
793             }
794         }
795         closedir($handle);
797         // Clean up the dataroot/filedir folder.
798         if (file_exists(self::get_dataroot() . '/filedir')) {
799             $handle = opendir(self::get_dataroot() . '/filedir');
800             while (false !== ($item = readdir($handle))) {
801                 if (in_array('filedir/' . $item, $childclassname::$datarootskiponreset)) {
802                     continue;
803                 }
804                 if (is_dir(self::get_dataroot()."/filedir/$item")) {
805                     remove_dir(self::get_dataroot()."/filedir/$item", false);
806                 } else {
807                     unlink(self::get_dataroot()."/filedir/$item");
808                 }
809             }
810             closedir($handle);
811         }
813         make_temp_directory('');
814         make_cache_directory('');
815         make_localcache_directory('');
816         // Reset the cache API so that it recreates it's required directories as well.
817         cache_factory::reset();
818         // Purge all data from the caches. This is required for consistency.
819         // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
820         // and now we will purge any other caches as well.
821         cache_helper::purge_all();
822     }
824     /**
825      * Gets a text-based site version description.
826      *
827      * @return string The site info
828      */
829     public static function get_site_info() {
830         global $CFG;
832         $output = '';
834         // All developers have to understand English, do not localise!
835         $env = self::get_environment();
837         $output .= "Moodle ".$env['moodleversion'];
838         if ($hash = self::get_git_hash()) {
839             $output .= ", $hash";
840         }
841         $output .= "\n";
843         // Add php version.
844         require_once($CFG->libdir.'/environmentlib.php');
845         $output .= "Php: ". normalize_version($env['phpversion']);
847         // Add database type and version.
848         $output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
850         // OS details.
851         $output .= ", OS: " . $env['os'] . "\n";
853         return $output;
854     }
856     /**
857      * Try to get current git hash of the Moodle in $CFG->dirroot.
858      * @return string null if unknown, sha1 hash if known
859      */
860     public static function get_git_hash() {
861         global $CFG;
863         // This is a bit naive, but it should mostly work for all platforms.
865         if (!file_exists("$CFG->dirroot/.git/HEAD")) {
866             return null;
867         }
869         $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
870         if ($headcontent === false) {
871             return null;
872         }
874         $headcontent = trim($headcontent);
876         // If it is pointing to a hash we return it directly.
877         if (strlen($headcontent) === 40) {
878             return $headcontent;
879         }
881         if (strpos($headcontent, 'ref: ') !== 0) {
882             return null;
883         }
885         $ref = substr($headcontent, 5);
887         if (!file_exists("$CFG->dirroot/.git/$ref")) {
888             return null;
889         }
891         $hash = file_get_contents("$CFG->dirroot/.git/$ref");
893         if ($hash === false) {
894             return null;
895         }
897         $hash = trim($hash);
899         if (strlen($hash) != 40) {
900             return null;
901         }
903         return $hash;
904     }
906     /**
907      * Set state of modified tables.
908      *
909      * @param string $sql sql which is updating the table.
910      */
911     public static function set_table_modified_by_sql($sql) {
912         global $DB;
914         $prefix = $DB->get_prefix();
916         preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
917         // Ignore random sql for testing like "XXUPDATE SET XSSD".
918         if (!empty($matches[1])) {
919             $table = trim($matches[1]);
920             $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
921             self::$tableupdated[$table] = true;
923             if (defined('BEHAT_SITE_RUNNING')) {
924                 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
925                 if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
926                     $tablesupdated[$table] = true;
927                 } else {
928                     $tablesupdated[$table] = true;
929                 }
930                 @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
931             }
932         }
933     }
935     /**
936      * Reset updated table list. This should be done after every reset.
937      */
938     public static function reset_updated_table_list() {
939         self::$tableupdated = array();
940     }
942     /**
943      * Returns the path to the file which holds list of tables updated in scenario.
944      * @return string
945      */
946     protected final static function get_tables_updated_by_scenario_list_path() {
947         return self::get_dataroot() . '/tablesupdatedbyscenario.txt';
948     }
950     /**
951      * Drop the whole test database
952      * @static
953      * @param bool $displayprogress
954      */
955     protected static function drop_database($displayprogress = false) {
956         global $DB;
958         $tables = $DB->get_tables(false);
959         if (isset($tables['config'])) {
960             // config always last to prevent problems with interrupted drops!
961             unset($tables['config']);
962             $tables['config'] = 'config';
963         }
965         if ($displayprogress) {
966             echo "Dropping tables:\n";
967         }
968         $dotsonline = 0;
969         foreach ($tables as $tablename) {
970             $table = new xmldb_table($tablename);
971             $DB->get_manager()->drop_table($table);
973             if ($dotsonline == 60) {
974                 if ($displayprogress) {
975                     echo "\n";
976                 }
977                 $dotsonline = 0;
978             }
979             if ($displayprogress) {
980                 echo '.';
981             }
982             $dotsonline += 1;
983         }
984         if ($displayprogress) {
985             echo "\n";
986         }
987     }
989     /**
990      * Drops the test framework dataroot
991      * @static
992      */
993     protected static function drop_dataroot() {
994         global $CFG;
996         $framework = self::get_framework();
997         $childclassname = $framework . '_util';
999         $files = scandir(self::get_dataroot() . '/'  . $framework);
1000         foreach ($files as $file) {
1001             if (in_array($file, $childclassname::$datarootskipondrop)) {
1002                 continue;
1003             }
1004             $path = self::get_dataroot() . '/' . $framework . '/' . $file;
1005             if (is_dir($path)) {
1006                 remove_dir($path, false);
1007             } else {
1008                 unlink($path);
1009             }
1010         }
1012         $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1013         if (file_exists($jsonfilepath)) {
1014             // Delete the json file.
1015             unlink($jsonfilepath);
1016             // Delete the dataroot filedir.
1017             remove_dir(self::get_dataroot() . '/filedir', false);
1018         }
1019     }
1021     /**
1022      * Skip the original dataroot files to not been reset.
1023      *
1024      * @static
1025      * @param string $utilclassname the util class name..
1026      */
1027     protected static function skip_original_data_files($utilclassname) {
1028         $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1029         if (file_exists($jsonfilepath)) {
1031             $listfiles = file_get_contents($jsonfilepath);
1033             // Mark each files as to not be reset.
1034             if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
1035                 $originaldatarootfiles = json_decode($listfiles);
1036                 // Keep the json file. Only drop_dataroot() should delete it.
1037                 $originaldatarootfiles[] = self::$originaldatafilesjson;
1038                 $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
1039                     $originaldatarootfiles);
1040                 self::$originaldatafilesjsonadded = true;
1041             }
1042         }
1043     }
1045     /**
1046      * Save the list of the original dataroot files into a json file.
1047      */
1048     protected static function save_original_data_files() {
1049         global $CFG;
1051         $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1053         // Save the original dataroot files if not done (only executed the first time).
1054         if (!file_exists($jsonfilepath)) {
1056             $listfiles = array();
1057             $listfiles['filedir/.'] = 'filedir/.';
1058             $listfiles['filedir/..'] = 'filedir/..';
1060             $filedir = self::get_dataroot() . '/filedir';
1061             if (file_exists($filedir)) {
1062                 $directory = new RecursiveDirectoryIterator($filedir);
1063                 foreach (new RecursiveIteratorIterator($directory) as $file) {
1064                     if ($file->isDir()) {
1065                         $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
1066                     } else {
1067                         $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
1068                     }
1069                     $listfiles[$key] = $key;
1070                 }
1071             }
1073             // Save the file list in a JSON file.
1074             $fp = fopen($jsonfilepath, 'w');
1075             fwrite($fp, json_encode(array_values($listfiles)));
1076             fclose($fp);
1077         }
1078     }
1080     /**
1081      * Return list of environment versions on which tests will run.
1082      * Environment includes:
1083      * - moodleversion
1084      * - phpversion
1085      * - dbtype
1086      * - dbversion
1087      * - os
1088      *
1089      * @return array
1090      */
1091     public static function get_environment() {
1092         global $CFG, $DB;
1094         $env = array();
1096         // Add moodle version.
1097         $release = null;
1098         require("$CFG->dirroot/version.php");
1099         $env['moodleversion'] = $release;
1101         // Add php version.
1102         $phpversion = phpversion();
1103         $env['phpversion'] = $phpversion;
1105         // Add database type and version.
1106         $dbtype = $CFG->dbtype;
1107         $dbinfo = $DB->get_server_info();
1108         $dbversion = $dbinfo['version'];
1109         $env['dbversion'] = $dbversion;
1111         // OS details.
1112         $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1113         $env['os'] = $osdetails;
1115         return $env;
1116     }