8588b1baf3cd0b88e7a04aec429fe91a2e9a340d
[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 int last value of db writes counter, used for db resetting
39      */
40     public static $lastdbwrites = 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 original structure of all database tables
64      */
65     protected static $sequencenames = null;
67     /**
68      * Returns the testing framework name
69      * @static
70      * @return string
71      */
72     protected static final function get_framework() {
73         $classname = get_called_class();
74         return substr($classname, 0, strpos($classname, '_'));
75     }
77     /**
78      * Get data generator
79      * @static
80      * @return testing_data_generator
81      */
82     public static function get_data_generator() {
83         if (is_null(self::$generator)) {
84             require_once(__DIR__.'/../generator/lib.php');
85             self::$generator = new testing_data_generator();
86         }
87         return self::$generator;
88     }
90     /**
91      * Does this site (db and dataroot) appear to be used for production?
92      * We try very hard to prevent accidental damage done to production servers!!
93      *
94      * @static
95      * @return bool
96      */
97     public static function is_test_site() {
98         global $DB, $CFG;
100         $framework = self::get_framework();
102         if (!file_exists($CFG->dataroot . '/' . $framework . 'testdir.txt')) {
103             // this is already tested in bootstrap script,
104             // but anyway presence of this file means the dataroot is for testing
105             return false;
106         }
108         $tables = $DB->get_tables(false);
109         if ($tables) {
110             if (!$DB->get_manager()->table_exists('config')) {
111                 return false;
112             }
113             if (!get_config('core', $framework . 'test')) {
114                 return false;
115             }
116         }
118         return true;
119     }
121     /**
122      * Returns whether test database and dataroot were created using the current version codebase
123      *
124      * @return boolean
125      */
126     protected static function is_test_data_updated() {
127         global $CFG;
129         $framework = self::get_framework();
131         $datarootpath = $CFG->dataroot . '/' . $framework;
132         if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
133             return false;
134         }
136         if (!file_exists($datarootpath . '/versionshash.txt')) {
137             return false;
138         }
140         $hash = self::get_version_hash();
141         $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
143         if ($hash !== $oldhash) {
144             return false;
145         }
147         $dbhash = get_config('core', $framework . 'test');
148         if ($hash !== $dbhash) {
149             return false;
150         }
152         return true;
153     }
155     /**
156      * Stores the status of the database
157      *
158      * Serializes the contents and the structure and
159      * stores it in the test framework space in dataroot
160      */
161     protected static function store_database_state() {
162         global $DB, $CFG;
164         $framework = self::get_framework();
166         // store data for all tables
167         $data = array();
168         $structure = array();
169         $tables = $DB->get_tables();
170         foreach ($tables as $table) {
171             $columns = $DB->get_columns($table);
172             $structure[$table] = $columns;
173             if (isset($columns['id']) and $columns['id']->auto_increment) {
174                 $data[$table] = $DB->get_records($table, array(), 'id ASC');
175             } else {
176                 // there should not be many of these
177                 $data[$table] = $DB->get_records($table, array());
178             }
179         }
180         $data = serialize($data);
181         $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
182         file_put_contents($datafile, $data);
183         testing_fix_file_permissions($datafile);
185         $structure = serialize($structure);
186         $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
187         file_put_contents($structurefile, $structure);
188         testing_fix_file_permissions($structurefile);
189     }
191     /**
192      * Stores the version hash in both database and dataroot
193      */
194     protected static function store_versions_hash() {
195         global $CFG;
197         $framework = self::get_framework();
198         $hash = self::get_version_hash();
200         // add test db flag
201         set_config($framework . 'test', $hash);
203         // hash all plugin versions - helps with very fast detection of db structure changes
204         $hashfile = $CFG->dataroot . '/' . $framework . '/versionshash.txt';
205         file_put_contents($hashfile, $hash);
206         testing_fix_file_permissions($hashfile);
207     }
209     /**
210      * Returns contents of all tables right after installation.
211      * @static
212      * @return array  $table=>$records
213      */
214     protected static function get_tabledata() {
215         global $CFG;
217         $framework = self::get_framework();
219         $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
220         if (!file_exists($datafile)) {
221             // Not initialised yet.
222             return array();
223         }
225         if (!isset(self::$tabledata)) {
226             $data = file_get_contents($datafile);
227             self::$tabledata = unserialize($data);
228         }
230         if (!is_array(self::$tabledata)) {
231             testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
232         }
234         return self::$tabledata;
235     }
237     /**
238      * Returns structure of all tables right after installation.
239      * @static
240      * @return array $table=>$records
241      */
242     public static function get_tablestructure() {
243         global $CFG;
245         $framework = self::get_framework();
247         $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
248         if (!file_exists($structurefile)) {
249             // Not initialised yet.
250             return array();
251         }
253         if (!isset(self::$tablestructure)) {
254             $data = file_get_contents($structurefile);
255             self::$tablestructure = unserialize($data);
256         }
258         if (!is_array(self::$tablestructure)) {
259             testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
260         }
262         return self::$tablestructure;
263     }
265     /**
266      * Returns the names of sequences for each autoincrementing id field in all standard tables.
267      * @static
268      * @return array $table=>$sequencename
269      */
270     public static function get_sequencenames() {
271         global $DB;
273         if (isset(self::$sequencenames)) {
274             return self::$sequencenames;
275         }
277         if (!$structure = self::get_tablestructure()) {
278             return array();
279         }
281         self::$sequencenames = array();
282         foreach ($structure as $table => $ignored) {
283             $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
284             if ($name !== false) {
285                 self::$sequencenames[$table] = $name;
286             }
287         }
289         return self::$sequencenames;
290     }
292     /**
293      * Returns list of tables that are unmodified and empty.
294      *
295      * @static
296      * @return array of table names, empty if unknown
297      */
298     protected static function guess_unmodified_empty_tables() {
299         global $DB;
301         $dbfamily = $DB->get_dbfamily();
303         if ($dbfamily === 'mysql') {
304             $empties = array();
305             $prefix = $DB->get_prefix();
306             $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
307             foreach ($rs as $info) {
308                 $table = strtolower($info->name);
309                 if (strpos($table, $prefix) !== 0) {
310                     // incorrect table match caused by _
311                     continue;
312                 }
313                 if (!is_null($info->auto_increment)) {
314                     $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
315                     if ($info->auto_increment == 1) {
316                         $empties[$table] = $table;
317                     }
318                 }
319             }
320             $rs->close();
321             return $empties;
323         } else if ($dbfamily === 'mssql') {
324             $empties = array();
325             $prefix = $DB->get_prefix();
326             $sql = "SELECT t.name
327                       FROM sys.identity_columns i
328                       JOIN sys.tables t ON t.object_id = i.object_id
329                      WHERE t.name LIKE ?
330                        AND i.name = 'id'
331                        AND i.last_value IS NULL";
332             $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
333             foreach ($rs as $info) {
334                 $table = strtolower($info->name);
335                 if (strpos($table, $prefix) !== 0) {
336                     // incorrect table match caused by _
337                     continue;
338                 }
339                 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
340                 $empties[$table] = $table;
341             }
342             $rs->close();
343             return $empties;
345         } else if ($dbfamily === 'oracle') {
346             $sequences = self::get_sequencenames();
347             $sequences = array_map('strtoupper', $sequences);
348             $lookup = array_flip($sequences);
349             $empties = array();
350             list($seqs, $params) = $DB->get_in_or_equal($sequences);
351             $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
352             $rs = $DB->get_recordset_sql($sql, $params);
353             foreach ($rs as $seq) {
354                 $table = $lookup[$seq->sequence_name];
355                 $empties[$table] = $table;
356             }
357             $rs->close();
358             return $empties;
360         } else {
361             return array();
362         }
363     }
365     /**
366      * Reset all database sequences to initial values.
367      *
368      * @static
369      * @param array $empties tables that are known to be unmodified and empty
370      * @return void
371      */
372     public static function reset_all_database_sequences(array $empties = null) {
373         global $DB;
375         if (!$data = self::get_tabledata()) {
376             // Not initialised yet.
377             return;
378         }
379         if (!$structure = self::get_tablestructure()) {
380             // Not initialised yet.
381             return;
382         }
384         $dbfamily = $DB->get_dbfamily();
385         if ($dbfamily === 'postgres') {
386             $queries = array();
387             $prefix = $DB->get_prefix();
388             foreach ($data as $table => $records) {
389                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
390                     if (empty($records)) {
391                         $nextid = 1;
392                     } else {
393                         $lastrecord = end($records);
394                         $nextid = $lastrecord->id + 1;
395                     }
396                     $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
397                 }
398             }
399             if ($queries) {
400                 $DB->change_database_structure(implode(';', $queries));
401             }
403         } else if ($dbfamily === 'mysql') {
404             $sequences = array();
405             $prefix = $DB->get_prefix();
406             $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
407             foreach ($rs as $info) {
408                 $table = strtolower($info->name);
409                 if (strpos($table, $prefix) !== 0) {
410                     // incorrect table match caused by _
411                     continue;
412                 }
413                 if (!is_null($info->auto_increment)) {
414                     $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
415                     $sequences[$table] = $info->auto_increment;
416                 }
417             }
418             $rs->close();
419             $prefix = $DB->get_prefix();
420             foreach ($data as $table => $records) {
421                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
422                     if (isset($sequences[$table])) {
423                         if (empty($records)) {
424                             $nextid = 1;
425                         } else {
426                             $lastrecord = end($records);
427                             $nextid = $lastrecord->id + 1;
428                         }
429                         if ($sequences[$table] != $nextid) {
430                             $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
431                         }
433                     } else {
434                         // some problem exists, fallback to standard code
435                         $DB->get_manager()->reset_sequence($table);
436                     }
437                 }
438             }
440         } else if ($dbfamily === 'oracle') {
441             $sequences = self::get_sequencenames();
442             $sequences = array_map('strtoupper', $sequences);
443             $lookup = array_flip($sequences);
445             $current = array();
446             list($seqs, $params) = $DB->get_in_or_equal($sequences);
447             $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
448             $rs = $DB->get_recordset_sql($sql, $params);
449             foreach ($rs as $seq) {
450                 $table = $lookup[$seq->sequence_name];
451                 $current[$table] = $seq->last_number;
452             }
453             $rs->close();
455             foreach ($data as $table => $records) {
456                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
457                     $lastrecord = end($records);
458                     if ($lastrecord) {
459                         $nextid = $lastrecord->id + 1;
460                     } else {
461                         $nextid = 1;
462                     }
463                     if (!isset($current[$table])) {
464                         $DB->get_manager()->reset_sequence($table);
465                     } else if ($nextid == $current[$table]) {
466                         continue;
467                     }
468                     // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
469                     $seqname = $sequences[$table];
470                     $cachesize = $DB->get_manager()->generator->sequence_cache_size;
471                     $DB->change_database_structure("DROP SEQUENCE $seqname");
472                     $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
473                 }
474             }
476         } else {
477             // note: does mssql support any kind of faster reset?
478             if (is_null($empties)) {
479                 $empties = self::guess_unmodified_empty_tables();
480             }
481             foreach ($data as $table => $records) {
482                 if (isset($empties[$table])) {
483                     continue;
484                 }
485                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
486                     $DB->get_manager()->reset_sequence($table);
487                 }
488             }
489         }
490     }
492     /**
493      * Resets the database
494      * @static
495      * @return boolean Returns whether database has been modified or not
496      */
497     public static function reset_database() {
498         global $DB;
500         if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
501             return false;
502         }
504         $tables = $DB->get_tables(false);
505         if (!$tables or empty($tables['config'])) {
506             // not installed yet
507             return false;
508         }
510         if (!$data = self::get_tabledata()) {
511             // not initialised yet
512             return false;
513         }
514         if (!$structure = self::get_tablestructure()) {
515             // not initialised yet
516             return false;
517         }
519         $empties = self::guess_unmodified_empty_tables();
521         foreach ($data as $table => $records) {
522             if (empty($records)) {
523                 if (isset($empties[$table])) {
524                     // table was not modified and is empty
525                 } else {
526                     $DB->delete_records($table, array());
527                 }
528                 continue;
529             }
531             if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
532                 $currentrecords = $DB->get_records($table, array(), 'id ASC');
533                 $changed = false;
534                 foreach ($records as $id => $record) {
535                     if (!isset($currentrecords[$id])) {
536                         $changed = true;
537                         break;
538                     }
539                     if ((array)$record != (array)$currentrecords[$id]) {
540                         $changed = true;
541                         break;
542                     }
543                     unset($currentrecords[$id]);
544                 }
545                 if (!$changed) {
546                     if ($currentrecords) {
547                         $lastrecord = end($records);
548                         $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
549                         continue;
550                     } else {
551                         continue;
552                     }
553                 }
554             }
556             $DB->delete_records($table, array());
557             foreach ($records as $record) {
558                 $DB->import_record($table, $record, false, true);
559             }
560         }
562         // reset all next record ids - aka sequences
563         self::reset_all_database_sequences($empties);
565         // remove extra tables
566         foreach ($tables as $table) {
567             if (!isset($data[$table])) {
568                 $DB->get_manager()->drop_table(new xmldb_table($table));
569             }
570         }
572         self::$lastdbwrites = $DB->perf_get_writes();
574         return true;
575     }
577     /**
578      * Purge dataroot directory
579      * @static
580      * @return void
581      */
582     public static function reset_dataroot() {
583         global $CFG;
585         $childclassname = self::get_framework() . '_util';
587         $handle = opendir($CFG->dataroot);
588         while (false !== ($item = readdir($handle))) {
589             if (in_array($item, $childclassname::$datarootskiponreset)) {
590                 continue;
591             }
592             if (is_dir("$CFG->dataroot/$item")) {
593                 remove_dir("$CFG->dataroot/$item", false);
594             } else {
595                 unlink("$CFG->dataroot/$item");
596             }
597         }
598         closedir($handle);
599         make_temp_directory('');
600         make_cache_directory('');
601         make_cache_directory('htmlpurifier');
602         // Reset the cache API so that it recreates it's required directories as well.
603         cache_factory::reset();
604         // Purge all data from the caches. This is required for consistency.
605         // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
606         // and now we will purge any other caches as well.
607         cache_helper::purge_all();
608     }
610     /**
611      * Drop the whole test database
612      * @static
613      * @param boolean $displayprogress
614      */
615     protected static function drop_database($displayprogress = false) {
616         global $DB;
618         $tables = $DB->get_tables(false);
619         if (isset($tables['config'])) {
620             // config always last to prevent problems with interrupted drops!
621             unset($tables['config']);
622             $tables['config'] = 'config';
623         }
625         if ($displayprogress) {
626             echo "Dropping tables:\n";
627         }
628         $dotsonline = 0;
629         foreach ($tables as $tablename) {
630             $table = new xmldb_table($tablename);
631             $DB->get_manager()->drop_table($table);
633             if ($dotsonline == 60) {
634                 if ($displayprogress) {
635                     echo "\n";
636                 }
637                 $dotsonline = 0;
638             }
639             if ($displayprogress) {
640                 echo '.';
641             }
642             $dotsonline += 1;
643         }
644         if ($displayprogress) {
645             echo "\n";
646         }
647     }
649     /**
650      * Drops the test framework dataroot
651      * @static
652      */
653     protected static function drop_dataroot() {
654         global $CFG;
656         $framework = self::get_framework();
657         $childclassname = $framework . '_util';
659         $files = scandir($CFG->dataroot . '/' . $framework);
660         foreach ($files as $file) {
661             if (in_array($file, $childclassname::$datarootskipondrop)) {
662                 continue;
663             }
664             $path = $CFG->dataroot . '/' . $framework . '/' . $file;
665             if (is_dir($path)) {
666                 remove_dir($path, false);
667             } else {
668                 unlink($path);
669             }
670         }
671     }
673     /**
674      * Reset all database tables to default values.
675      * @static
676      * @return bool true if reset done, false if skipped
677      */
678     /**
679      * Calculate unique version hash for all plugins and core.
680      * @static
681      * @return string sha1 hash
682      */
683     public static function get_version_hash() {
684         global $CFG;
686         if (self::$versionhash) {
687             return self::$versionhash;
688         }
690         $versions = array();
692         // main version first
693         $version = null;
694         include($CFG->dirroot.'/version.php');
695         $versions['core'] = $version;
697         // modules
698         $mods = get_plugin_list('mod');
699         ksort($mods);
700         foreach ($mods as $mod => $fullmod) {
701             $module = new stdClass();
702             $module->version = null;
703             include($fullmod.'/version.php');
704             $versions[$mod] = $module->version;
705         }
707         // now the rest of plugins
708         $plugintypes = get_plugin_types();
709         unset($plugintypes['mod']);
710         ksort($plugintypes);
711         foreach ($plugintypes as $type => $unused) {
712             $plugs = get_plugin_list($type);
713             ksort($plugs);
714             foreach ($plugs as $plug => $fullplug) {
715                 $plugin = new stdClass();
716                 $plugin->version = null;
717                 @include($fullplug.'/version.php');
718                 $versions[$plug] = $plugin->version;
719             }
720         }
722         self::$versionhash = sha1(serialize($versions));
724         return self::$versionhash;
725     }