MDL-60915 core_dml: fix miscellaneous incorrect recordset usage
[moodle.git] / lib / testing / classes / util.php
blob533200850a612043ab1183e545408da9e99430eb
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.
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
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
27 /**
28 * Utils for test sites creation
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
35 abstract class testing_util {
37 /**
38 * @var string dataroot (likely to be $CFG->dataroot).
40 private static $dataroot = null;
42 /**
43 * @var testing_data_generator
45 protected static $generator = null;
47 /**
48 * @var string current version hash from php files
50 protected static $versionhash = null;
52 /**
53 * @var array original content of all database tables
55 protected static $tabledata = null;
57 /**
58 * @var array original structure of all database tables
60 protected static $tablestructure = null;
62 /**
63 * @var array keep list of sequenceid used in a table.
65 private static $tablesequences = array();
67 /**
68 * @var array list of updated tables.
70 public static $tableupdated = array();
72 /**
73 * @var array original structure of all database tables
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.
80 private static $originaldatafilesjson = 'originaldatafiles.json';
82 /**
83 * @var boolean set to true once $originaldatafilesjson file is created.
85 private static $originaldatafilesjsonadded = false;
87 /**
88 * @var int next sequence value for a single test cycle.
90 protected static $sequencenextstartingid = null;
92 /**
93 * Return the name of the JSON file containing the init filenames.
95 * @static
96 * @return string
98 public static function get_originaldatafilesjson() {
99 return self::$originaldatafilesjson;
103 * Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
105 * @static
106 * @return string the dataroot.
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;
116 return self::$dataroot;
120 * Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
122 * @param string $dataroot the dataroot of the test framework.
123 * @static
125 public static function set_dataroot($dataroot) {
126 self::$dataroot = $dataroot;
130 * Returns the testing framework name
131 * @static
132 * @return string
134 protected static final function get_framework() {
135 $classname = get_called_class();
136 return substr($classname, 0, strpos($classname, '_'));
140 * Get data generator
141 * @static
142 * @return testing_data_generator
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();
149 return self::$generator;
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!!
156 * @static
157 * @return bool
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;
170 $tables = $DB->get_tables(false);
171 if ($tables) {
172 if (!$DB->get_manager()->table_exists('config')) {
173 return false;
175 if (!get_config('core', $framework . 'test')) {
176 return false;
180 return true;
184 * Returns whether test database and dataroot were created using the current version codebase
186 * @return bool
188 public static function is_test_data_updated() {
189 global $DB;
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;
198 if (!file_exists($datarootpath . '/versionshash.txt')) {
199 return false;
202 $hash = core_component::get_all_versions_hash();
203 $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
205 if ($hash !== $oldhash) {
206 return false;
209 // A direct database request must be used to avoid any possible caching of an older value.
210 $dbhash = $DB->get_field('config', 'value', array('name' => $framework . 'test'));
211 if ($hash !== $dbhash) {
212 return false;
215 return true;
219 * Stores the status of the database
221 * Serializes the contents and the structure and
222 * stores it in the test framework space in dataroot
224 protected static function store_database_state() {
225 global $DB, $CFG;
227 $framework = self::get_framework();
229 // store data for all tables
230 $data = array();
231 $structure = array();
232 $tables = $DB->get_tables();
233 foreach ($tables as $table) {
234 $columns = $DB->get_columns($table);
235 $structure[$table] = $columns;
236 if (isset($columns['id']) and $columns['id']->auto_increment) {
237 $data[$table] = $DB->get_records($table, array(), 'id ASC');
238 } else {
239 // there should not be many of these
240 $data[$table] = $DB->get_records($table, array());
243 $data = serialize($data);
244 $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
245 file_put_contents($datafile, $data);
246 testing_fix_file_permissions($datafile);
248 $structure = serialize($structure);
249 $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
250 file_put_contents($structurefile, $structure);
251 testing_fix_file_permissions($structurefile);
255 * Stores the version hash in both database and dataroot
257 protected static function store_versions_hash() {
258 global $CFG;
260 $framework = self::get_framework();
261 $hash = core_component::get_all_versions_hash();
263 // add test db flag
264 set_config($framework . 'test', $hash);
266 // hash all plugin versions - helps with very fast detection of db structure changes
267 $hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt';
268 file_put_contents($hashfile, $hash);
269 testing_fix_file_permissions($hashfile);
273 * Returns contents of all tables right after installation.
274 * @static
275 * @return array $table=>$records
277 protected static function get_tabledata() {
278 if (!isset(self::$tabledata)) {
279 $framework = self::get_framework();
281 $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
282 if (!file_exists($datafile)) {
283 // Not initialised yet.
284 return array();
287 $data = file_get_contents($datafile);
288 self::$tabledata = unserialize($data);
291 if (!is_array(self::$tabledata)) {
292 testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
295 return self::$tabledata;
299 * Returns structure of all tables right after installation.
300 * @static
301 * @return array $table=>$records
303 public static function get_tablestructure() {
304 if (!isset(self::$tablestructure)) {
305 $framework = self::get_framework();
307 $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
308 if (!file_exists($structurefile)) {
309 // Not initialised yet.
310 return array();
313 $data = file_get_contents($structurefile);
314 self::$tablestructure = unserialize($data);
317 if (!is_array(self::$tablestructure)) {
318 testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
321 return self::$tablestructure;
325 * Returns the names of sequences for each autoincrementing id field in all standard tables.
326 * @static
327 * @return array $table=>$sequencename
329 public static function get_sequencenames() {
330 global $DB;
332 if (isset(self::$sequencenames)) {
333 return self::$sequencenames;
336 if (!$structure = self::get_tablestructure()) {
337 return array();
340 self::$sequencenames = array();
341 foreach ($structure as $table => $ignored) {
342 $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
343 if ($name !== false) {
344 self::$sequencenames[$table] = $name;
348 return self::$sequencenames;
352 * Returns list of tables that are unmodified and empty.
354 * @static
355 * @return array of table names, empty if unknown
357 protected static function guess_unmodified_empty_tables() {
358 global $DB;
360 $dbfamily = $DB->get_dbfamily();
362 if ($dbfamily === 'mysql') {
363 $empties = array();
364 $prefix = $DB->get_prefix();
365 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
366 foreach ($rs as $info) {
367 $table = strtolower($info->name);
368 if (strpos($table, $prefix) !== 0) {
369 // incorrect table match caused by _
370 continue;
373 if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
374 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
375 $empties[$table] = $table;
378 $rs->close();
379 return $empties;
381 } else if ($dbfamily === 'mssql') {
382 $empties = array();
383 $prefix = $DB->get_prefix();
384 $sql = "SELECT t.name
385 FROM sys.identity_columns i
386 JOIN sys.tables t ON t.object_id = i.object_id
387 WHERE t.name LIKE ?
388 AND i.name = 'id'
389 AND i.last_value IS NULL";
390 $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
391 foreach ($rs as $info) {
392 $table = strtolower($info->name);
393 if (strpos($table, $prefix) !== 0) {
394 // incorrect table match caused by _
395 continue;
397 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
398 $empties[$table] = $table;
400 $rs->close();
401 return $empties;
403 } else if ($dbfamily === 'oracle') {
404 $sequences = self::get_sequencenames();
405 $sequences = array_map('strtoupper', $sequences);
406 $lookup = array_flip($sequences);
407 $empties = array();
408 list($seqs, $params) = $DB->get_in_or_equal($sequences);
409 $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
410 $rs = $DB->get_recordset_sql($sql, $params);
411 foreach ($rs as $seq) {
412 $table = $lookup[$seq->sequence_name];
413 $empties[$table] = $table;
415 $rs->close();
416 return $empties;
418 } else {
419 return array();
424 * Determine the next unique starting id sequences.
426 * @static
427 * @param array $records The records to use to determine the starting value for the table.
428 * @param string $table table name.
429 * @return int The value the sequence should be set to.
431 private static function get_next_sequence_starting_value($records, $table) {
432 if (isset(self::$tablesequences[$table])) {
433 return self::$tablesequences[$table];
436 $id = self::$sequencenextstartingid;
438 // If there are records, calculate the minimum id we can use.
439 // It must be bigger than the last record's id.
440 if (!empty($records)) {
441 $lastrecord = end($records);
442 $id = max($id, $lastrecord->id + 1);
445 self::$sequencenextstartingid = $id + 1000;
447 self::$tablesequences[$table] = $id;
449 return $id;
453 * Reset all database sequences to initial values.
455 * @static
456 * @param array $empties tables that are known to be unmodified and empty
457 * @return void
459 public static function reset_all_database_sequences(array $empties = null) {
460 global $DB;
462 if (!$data = self::get_tabledata()) {
463 // Not initialised yet.
464 return;
466 if (!$structure = self::get_tablestructure()) {
467 // Not initialised yet.
468 return;
471 $updatedtables = self::$tableupdated;
473 // If all starting Id's are the same, it's difficult to detect coding and testing
474 // errors that use the incorrect id in tests. The classic case is cmid vs instance id.
475 // To reduce the chance of the coding error, we start sequences at different values where possible.
476 // In a attempt to avoid tables with existing id's we start at a high number.
477 // Reset the value each time all database sequences are reset.
478 if (defined('PHPUNIT_SEQUENCE_START') and PHPUNIT_SEQUENCE_START) {
479 self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
480 } else {
481 self::$sequencenextstartingid = 100000;
484 $dbfamily = $DB->get_dbfamily();
485 if ($dbfamily === 'postgres') {
486 $queries = array();
487 $prefix = $DB->get_prefix();
488 foreach ($data as $table => $records) {
489 // If table is not modified then no need to do anything.
490 if (!isset($updatedtables[$table])) {
491 continue;
493 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
494 $nextid = self::get_next_sequence_starting_value($records, $table);
495 $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
498 if ($queries) {
499 $DB->change_database_structure(implode(';', $queries));
502 } else if ($dbfamily === 'mysql') {
503 $queries = array();
504 $sequences = array();
505 $prefix = $DB->get_prefix();
506 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
507 foreach ($rs as $info) {
508 $table = strtolower($info->name);
509 if (strpos($table, $prefix) !== 0) {
510 // incorrect table match caused by _
511 continue;
513 if (!is_null($info->auto_increment)) {
514 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
515 $sequences[$table] = $info->auto_increment;
518 $rs->close();
519 $prefix = $DB->get_prefix();
520 foreach ($data as $table => $records) {
521 // If table is not modified then no need to do anything.
522 if (!isset($updatedtables[$table])) {
523 continue;
525 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
526 if (isset($sequences[$table])) {
527 $nextid = self::get_next_sequence_starting_value($records, $table);
528 if ($sequences[$table] != $nextid) {
529 $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
531 } else {
532 // some problem exists, fallback to standard code
533 $DB->get_manager()->reset_sequence($table);
537 if ($queries) {
538 $DB->change_database_structure(implode(';', $queries));
541 } else if ($dbfamily === 'oracle') {
542 $sequences = self::get_sequencenames();
543 $sequences = array_map('strtoupper', $sequences);
544 $lookup = array_flip($sequences);
546 $current = array();
547 list($seqs, $params) = $DB->get_in_or_equal($sequences);
548 $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
549 $rs = $DB->get_recordset_sql($sql, $params);
550 foreach ($rs as $seq) {
551 $table = $lookup[$seq->sequence_name];
552 $current[$table] = $seq->last_number;
554 $rs->close();
556 foreach ($data as $table => $records) {
557 // If table is not modified then no need to do anything.
558 if (!isset($updatedtables[$table])) {
559 continue;
561 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
562 $lastrecord = end($records);
563 if ($lastrecord) {
564 $nextid = $lastrecord->id + 1;
565 } else {
566 $nextid = 1;
568 if (!isset($current[$table])) {
569 $DB->get_manager()->reset_sequence($table);
570 } else if ($nextid == $current[$table]) {
571 continue;
573 // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
574 $seqname = $sequences[$table];
575 $cachesize = $DB->get_manager()->generator->sequence_cache_size;
576 $DB->change_database_structure("DROP SEQUENCE $seqname");
577 $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
581 } else {
582 // note: does mssql support any kind of faster reset?
583 // This also implies mssql will not use unique sequence values.
584 if (is_null($empties) and (empty($updatedtables))) {
585 $empties = self::guess_unmodified_empty_tables();
587 foreach ($data as $table => $records) {
588 // If table is not modified then no need to do anything.
589 if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
590 continue;
592 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
593 $DB->get_manager()->reset_sequence($table);
600 * Reset all database tables to default values.
601 * @static
602 * @return bool true if reset done, false if skipped
604 public static function reset_database() {
605 global $DB;
607 $tables = $DB->get_tables(false);
608 if (!$tables or empty($tables['config'])) {
609 // not installed yet
610 return false;
613 if (!$data = self::get_tabledata()) {
614 // not initialised yet
615 return false;
617 if (!$structure = self::get_tablestructure()) {
618 // not initialised yet
619 return false;
622 $empties = array();
623 // Use local copy of self::$tableupdated, as list gets updated in for loop.
624 $updatedtables = self::$tableupdated;
626 // If empty tablesequences list then it's the very first run.
627 if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
628 // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
629 $empties = self::guess_unmodified_empty_tables();
632 // Check if any table has been modified by behat selenium process.
633 if (defined('BEHAT_SITE_RUNNING')) {
634 // Crazy way to reset :(.
635 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
636 if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
637 self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
638 unlink($tablesupdatedfile);
640 $updatedtables = self::$tableupdated;
643 $borkedmysql = false;
644 if ($DB->get_dbfamily() === 'mysql') {
645 $version = $DB->get_server_info();
646 if (version_compare($version['version'], '5.6.0') == 1 and version_compare($version['version'], '5.6.16') == -1) {
647 // Everything that comes from Oracle is evil!
649 // See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
650 // You cannot reset the counter to a value less than or equal to to the value that is currently in use.
652 // From 5.6.16 release notes:
653 // InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
654 // (Bug #17250787, Bug #69882)
655 $borkedmysql = true;
657 } else if (version_compare($version['version'], '10.0.0') == 1) {
658 // And MariaDB is no better!
659 // Let's hope they pick the patch sometime later...
660 $borkedmysql = true;
664 if ($borkedmysql) {
665 $mysqlsequences = array();
666 $prefix = $DB->get_prefix();
667 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
668 foreach ($rs as $info) {
669 $table = strtolower($info->name);
670 if (strpos($table, $prefix) !== 0) {
671 // Incorrect table match caused by _ char.
672 continue;
674 if (!is_null($info->auto_increment)) {
675 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
676 $mysqlsequences[$table] = $info->auto_increment;
679 $rs->close();
682 foreach ($data as $table => $records) {
683 // If table is not modified then no need to do anything.
684 // $updatedtables tables is set after the first run, so check before checking for specific table update.
685 if (!empty($updatedtables) && !isset($updatedtables[$table])) {
686 continue;
689 if ($borkedmysql) {
690 if (empty($records)) {
691 if (!isset($empties[$table])) {
692 // Table has been modified and is not empty.
693 $DB->delete_records($table, null);
695 continue;
698 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
699 $current = $DB->get_records($table, array(), 'id ASC');
700 if ($current == $records) {
701 if (isset($mysqlsequences[$table]) and $mysqlsequences[$table] == $structure[$table]['id']->auto_increment) {
702 continue;
707 // Use TRUNCATE as a workaround and reinsert everything.
708 $DB->delete_records($table, null);
709 foreach ($records as $record) {
710 $DB->import_record($table, $record, false, true);
712 continue;
715 if (empty($records)) {
716 if (!isset($empties[$table])) {
717 // Table has been modified and is not empty.
718 $DB->delete_records($table, array());
720 continue;
723 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
724 $currentrecords = $DB->get_records($table, array(), 'id ASC');
725 $changed = false;
726 foreach ($records as $id => $record) {
727 if (!isset($currentrecords[$id])) {
728 $changed = true;
729 break;
731 if ((array)$record != (array)$currentrecords[$id]) {
732 $changed = true;
733 break;
735 unset($currentrecords[$id]);
737 if (!$changed) {
738 if ($currentrecords) {
739 $lastrecord = end($records);
740 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
741 continue;
742 } else {
743 continue;
748 $DB->delete_records($table, array());
749 foreach ($records as $record) {
750 $DB->import_record($table, $record, false, true);
754 // reset all next record ids - aka sequences
755 self::reset_all_database_sequences($empties);
757 // remove extra tables
758 foreach ($tables as $table) {
759 if (!isset($data[$table])) {
760 $DB->get_manager()->drop_table(new xmldb_table($table));
764 self::reset_updated_table_list();
766 return true;
770 * Purge dataroot directory
771 * @static
772 * @return void
774 public static function reset_dataroot() {
775 global $CFG;
777 $childclassname = self::get_framework() . '_util';
779 // Do not delete automatically installed files.
780 self::skip_original_data_files($childclassname);
782 // Clear file status cache, before checking file_exists.
783 clearstatcache();
785 // Clean up the dataroot folder.
786 $handle = opendir(self::get_dataroot());
787 while (false !== ($item = readdir($handle))) {
788 if (in_array($item, $childclassname::$datarootskiponreset)) {
789 continue;
791 if (is_dir(self::get_dataroot()."/$item")) {
792 remove_dir(self::get_dataroot()."/$item", false);
793 } else {
794 unlink(self::get_dataroot()."/$item");
797 closedir($handle);
799 // Clean up the dataroot/filedir folder.
800 if (file_exists(self::get_dataroot() . '/filedir')) {
801 $handle = opendir(self::get_dataroot() . '/filedir');
802 while (false !== ($item = readdir($handle))) {
803 if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) {
804 continue;
806 if (is_dir(self::get_dataroot()."/filedir/$item")) {
807 remove_dir(self::get_dataroot()."/filedir/$item", false);
808 } else {
809 unlink(self::get_dataroot()."/filedir/$item");
812 closedir($handle);
815 make_temp_directory('');
816 make_cache_directory('');
817 make_localcache_directory('');
818 // Purge all data from the caches. This is required for consistency between tests.
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. This must be done before the cache_factory::reset() as that
821 // removes all definitions of caches and purge does not have valid caches to operate on.
822 cache_helper::purge_all();
823 // Reset the cache API so that it recreates it's required directories as well.
824 cache_factory::reset();
828 * Gets a text-based site version description.
830 * @return string The site info
832 public static function get_site_info() {
833 global $CFG;
835 $output = '';
837 // All developers have to understand English, do not localise!
838 $env = self::get_environment();
840 $output .= "Moodle ".$env['moodleversion'];
841 if ($hash = self::get_git_hash()) {
842 $output .= ", $hash";
844 $output .= "\n";
846 // Add php version.
847 require_once($CFG->libdir.'/environmentlib.php');
848 $output .= "Php: ". normalize_version($env['phpversion']);
850 // Add database type and version.
851 $output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
853 // OS details.
854 $output .= ", OS: " . $env['os'] . "\n";
856 return $output;
860 * Try to get current git hash of the Moodle in $CFG->dirroot.
861 * @return string null if unknown, sha1 hash if known
863 public static function get_git_hash() {
864 global $CFG;
866 // This is a bit naive, but it should mostly work for all platforms.
868 if (!file_exists("$CFG->dirroot/.git/HEAD")) {
869 return null;
872 $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
873 if ($headcontent === false) {
874 return null;
877 $headcontent = trim($headcontent);
879 // If it is pointing to a hash we return it directly.
880 if (strlen($headcontent) === 40) {
881 return $headcontent;
884 if (strpos($headcontent, 'ref: ') !== 0) {
885 return null;
888 $ref = substr($headcontent, 5);
890 if (!file_exists("$CFG->dirroot/.git/$ref")) {
891 return null;
894 $hash = file_get_contents("$CFG->dirroot/.git/$ref");
896 if ($hash === false) {
897 return null;
900 $hash = trim($hash);
902 if (strlen($hash) != 40) {
903 return null;
906 return $hash;
910 * Set state of modified tables.
912 * @param string $sql sql which is updating the table.
914 public static function set_table_modified_by_sql($sql) {
915 global $DB;
917 $prefix = $DB->get_prefix();
919 preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
920 // Ignore random sql for testing like "XXUPDATE SET XSSD".
921 if (!empty($matches[1])) {
922 $table = trim($matches[1]);
923 $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
924 self::$tableupdated[$table] = true;
926 if (defined('BEHAT_SITE_RUNNING')) {
927 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
928 if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
929 $tablesupdated[$table] = true;
930 } else {
931 $tablesupdated[$table] = true;
933 @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
939 * Reset updated table list. This should be done after every reset.
941 public static function reset_updated_table_list() {
942 self::$tableupdated = array();
946 * Delete tablesupdatedbyscenario file. This should be called before suite,
947 * to ensure full db reset.
949 public static function clean_tables_updated_by_scenario_list() {
950 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
951 if (file_exists($tablesupdatedfile)) {
952 unlink($tablesupdatedfile);
955 // Reset static cache of cli process.
956 self::reset_updated_table_list();
960 * Returns the path to the file which holds list of tables updated in scenario.
961 * @return string
963 protected final static function get_tables_updated_by_scenario_list_path() {
964 return self::get_dataroot() . '/tablesupdatedbyscenario.json';
968 * Drop the whole test database
969 * @static
970 * @param bool $displayprogress
972 protected static function drop_database($displayprogress = false) {
973 global $DB;
975 $tables = $DB->get_tables(false);
976 if (isset($tables['config'])) {
977 // config always last to prevent problems with interrupted drops!
978 unset($tables['config']);
979 $tables['config'] = 'config';
982 if ($displayprogress) {
983 echo "Dropping tables:\n";
985 $dotsonline = 0;
986 foreach ($tables as $tablename) {
987 $table = new xmldb_table($tablename);
988 $DB->get_manager()->drop_table($table);
990 if ($dotsonline == 60) {
991 if ($displayprogress) {
992 echo "\n";
994 $dotsonline = 0;
996 if ($displayprogress) {
997 echo '.';
999 $dotsonline += 1;
1001 if ($displayprogress) {
1002 echo "\n";
1007 * Drops the test framework dataroot
1008 * @static
1010 protected static function drop_dataroot() {
1011 global $CFG;
1013 $framework = self::get_framework();
1014 $childclassname = $framework . '_util';
1016 $files = scandir(self::get_dataroot() . '/' . $framework);
1017 foreach ($files as $file) {
1018 if (in_array($file, $childclassname::$datarootskipondrop)) {
1019 continue;
1021 $path = self::get_dataroot() . '/' . $framework . '/' . $file;
1022 if (is_dir($path)) {
1023 remove_dir($path, false);
1024 } else {
1025 unlink($path);
1029 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1030 if (file_exists($jsonfilepath)) {
1031 // Delete the json file.
1032 unlink($jsonfilepath);
1033 // Delete the dataroot filedir.
1034 remove_dir(self::get_dataroot() . '/filedir', false);
1039 * Skip the original dataroot files to not been reset.
1041 * @static
1042 * @param string $utilclassname the util class name..
1044 protected static function skip_original_data_files($utilclassname) {
1045 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1046 if (file_exists($jsonfilepath)) {
1048 $listfiles = file_get_contents($jsonfilepath);
1050 // Mark each files as to not be reset.
1051 if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
1052 $originaldatarootfiles = json_decode($listfiles);
1053 // Keep the json file. Only drop_dataroot() should delete it.
1054 $originaldatarootfiles[] = self::$originaldatafilesjson;
1055 $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
1056 $originaldatarootfiles);
1057 self::$originaldatafilesjsonadded = true;
1063 * Save the list of the original dataroot files into a json file.
1065 protected static function save_original_data_files() {
1066 global $CFG;
1068 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1070 // Save the original dataroot files if not done (only executed the first time).
1071 if (!file_exists($jsonfilepath)) {
1073 $listfiles = array();
1074 $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.';
1075 $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..';
1076 $listfiles[$currentdir] = $currentdir;
1077 $listfiles[$parentdir] = $parentdir;
1079 $filedir = self::get_dataroot() . '/filedir';
1080 if (file_exists($filedir)) {
1081 $directory = new RecursiveDirectoryIterator($filedir);
1082 foreach (new RecursiveIteratorIterator($directory) as $file) {
1083 if ($file->isDir()) {
1084 $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
1085 } else {
1086 $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
1088 $listfiles[$key] = $key;
1092 // Save the file list in a JSON file.
1093 $fp = fopen($jsonfilepath, 'w');
1094 fwrite($fp, json_encode(array_values($listfiles)));
1095 fclose($fp);
1100 * Return list of environment versions on which tests will run.
1101 * Environment includes:
1102 * - moodleversion
1103 * - phpversion
1104 * - dbtype
1105 * - dbversion
1106 * - os
1108 * @return array
1110 public static function get_environment() {
1111 global $CFG, $DB;
1113 $env = array();
1115 // Add moodle version.
1116 $release = null;
1117 require("$CFG->dirroot/version.php");
1118 $env['moodleversion'] = $release;
1120 // Add php version.
1121 $phpversion = phpversion();
1122 $env['phpversion'] = $phpversion;
1124 // Add database type and version.
1125 $dbtype = $CFG->dbtype;
1126 $dbinfo = $DB->get_server_info();
1127 $dbversion = $dbinfo['version'];
1128 $env['dbtype'] = $dbtype;
1129 $env['dbversion'] = $dbversion;
1131 // OS details.
1132 $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1133 $env['os'] = $osdetails;
1135 return $env;