Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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/>. 16 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 */ 26 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 { 36 37 /** 38 * @var string dataroot (likely to be $CFG->dataroot). 39 */ 40 private static $dataroot = null; 41 42 /** 43 * @var testing_data_generator 44 */ 45 protected static $generator = null; 46 47 /** 48 * @var string current version hash from php files 49 */ 50 protected static $versionhash = null; 51 52 /** 53 * @var array original content of all database tables 54 */ 55 protected static $tabledata = null; 56 57 /** 58 * @var array original structure of all database tables 59 */ 60 protected static $tablestructure = null; 61 62 /** 63 * @var array keep list of sequenceid used in a table. 64 */ 65 private static $tablesequences = array(); 66 67 /** 68 * @var array list of updated tables. 69 */ 70 public static $tableupdated = array(); 71 72 /** 73 * @var array original structure of all database tables 74 */ 75 protected static $sequencenames = null; 76 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'; 81 82 /** 83 * @var boolean set to true once $originaldatafilesjson file is created. 84 */ 85 private static $originaldatafilesjsonadded = false; 86 87 /** 88 * @var int next sequence value for a single test cycle. 89 */ 90 protected static $sequencenextstartingid = null; 91 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 } 101 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; 110 111 // By default it's the test framework dataroot. 112 if (empty(self::$dataroot)) { 113 self::$dataroot = $CFG->dataroot; 114 } 115 116 return self::$dataroot; 117 } 118 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 } 128 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 } 138 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 } 151 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; 161 162 $framework = self::get_framework(); 163 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 } 169 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 } 179 180 return true; 181 } 182 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 $DB; 190 191 $framework = self::get_framework(); 192 193 $datarootpath = self::get_dataroot() . '/' . $framework; 194 if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) { 195 return false; 196 } 197 198 if (!file_exists($datarootpath . '/versionshash.txt')) { 199 return false; 200 } 201 202 $hash = core_component::get_all_versions_hash(); 203 $oldhash = file_get_contents($datarootpath . '/versionshash.txt'); 204 205 if ($hash !== $oldhash) { 206 return false; 207 } 208 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; 213 } 214 215 return true; 216 } 217 218 /** 219 * Stores the status of the database 220 * 221 * Serializes the contents and the structure and 222 * stores it in the test framework space in dataroot 223 */ 224 protected static function store_database_state() { 225 global $DB, $CFG; 226 227 $framework = self::get_framework(); 228 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()); 241 } 242 } 243 $data = serialize($data); 244 $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser'; 245 file_put_contents($datafile, $data); 246 testing_fix_file_permissions($datafile); 247 248 $structure = serialize($structure); 249 $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser'; 250 file_put_contents($structurefile, $structure); 251 testing_fix_file_permissions($structurefile); 252 } 253 254 /** 255 * Stores the version hash in both database and dataroot 256 */ 257 protected static function store_versions_hash() { 258 global $CFG; 259 260 $framework = self::get_framework(); 261 $hash = core_component::get_all_versions_hash(); 262 263 // add test db flag 264 set_config($framework . 'test', $hash); 265 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); 270 } 271 272 /** 273 * Returns contents of all tables right after installation. 274 * @static 275 * @return array $table=>$records 276 */ 277 protected static function get_tabledata() { 278 if (!isset(self::$tabledata)) { 279 $framework = self::get_framework(); 280 281 $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser'; 282 if (!file_exists($datafile)) { 283 // Not initialised yet. 284 return array(); 285 } 286 287 $data = file_get_contents($datafile); 288 self::$tabledata = unserialize($data); 289 } 290 291 if (!is_array(self::$tabledata)) { 292 testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.'); 293 } 294 295 return self::$tabledata; 296 } 297 298 /** 299 * Returns structure of all tables right after installation. 300 * @static 301 * @return array $table=>$records 302 */ 303 public static function get_tablestructure() { 304 if (!isset(self::$tablestructure)) { 305 $framework = self::get_framework(); 306 307 $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser'; 308 if (!file_exists($structurefile)) { 309 // Not initialised yet. 310 return array(); 311 } 312 313 $data = file_get_contents($structurefile); 314 self::$tablestructure = unserialize($data); 315 } 316 317 if (!is_array(self::$tablestructure)) { 318 testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.'); 319 } 320 321 return self::$tablestructure; 322 } 323 324 /** 325 * Returns the names of sequences for each autoincrementing id field in all standard tables. 326 * @static 327 * @return array $table=>$sequencename 328 */ 329 public static function get_sequencenames() { 330 global $DB; 331 332 if (isset(self::$sequencenames)) { 333 return self::$sequencenames; 334 } 335 336 if (!$structure = self::get_tablestructure()) { 337 return array(); 338 } 339 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; 345 } 346 } 347 348 return self::$sequencenames; 349 } 350 351 /** 352 * Returns list of tables that are unmodified and empty. 353 * 354 * @static 355 * @return array of table names, empty if unknown 356 */ 357 protected static function guess_unmodified_empty_tables() { 358 global $DB; 359 360 $dbfamily = $DB->get_dbfamily(); 361 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; 371 } 372 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; 376 } 377 } 378 $rs->close(); 379 return $empties; 380 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; 396 } 397 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); 398 $empties[$table] = $table; 399 } 400 $rs->close(); 401 return $empties; 402 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; 414 } 415 $rs->close(); 416 return $empties; 417 418 } else { 419 return array(); 420 } 421 } 422 423 /** 424 * Determine the next unique starting id sequences. 425 * 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. 430 */ 431 private static function get_next_sequence_starting_value($records, $table) { 432 if (isset(self::$tablesequences[$table])) { 433 return self::$tablesequences[$table]; 434 } 435 436 $id = self::$sequencenextstartingid; 437 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); 443 } 444 445 self::$sequencenextstartingid = $id + 1000; 446 447 self::$tablesequences[$table] = $id; 448 449 return $id; 450 } 451 452 /** 453 * Reset all database sequences to initial values. 454 * 455 * @static 456 * @param array $empties tables that are known to be unmodified and empty 457 * @return void 458 */ 459 public static function reset_all_database_sequences(array $empties = null) { 460 global $DB; 461 462 if (!$data = self::get_tabledata()) { 463 // Not initialised yet. 464 return; 465 } 466 if (!$structure = self::get_tablestructure()) { 467 // Not initialised yet. 468 return; 469 } 470 471 $updatedtables = self::$tableupdated; 472 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; 482 } 483 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; 492 } 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"; 496 } 497 } 498 if ($queries) { 499 $DB->change_database_structure(implode(';', $queries)); 500 } 501 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; 512 } 513 if (!is_null($info->auto_increment)) { 514 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); 515 $sequences[$table] = $info->auto_increment; 516 } 517 } 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; 524 } 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"; 530 } 531 } else { 532 // some problem exists, fallback to standard code 533 $DB->get_manager()->reset_sequence($table); 534 } 535 } 536 } 537 if ($queries) { 538 $DB->change_database_structure(implode(';', $queries)); 539 } 540 541 } else if ($dbfamily === 'oracle') { 542 $sequences = self::get_sequencenames(); 543 $sequences = array_map('strtoupper', $sequences); 544 $lookup = array_flip($sequences); 545 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; 553 } 554 $rs->close(); 555 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; 560 } 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; 567 } 568 if (!isset($current[$table])) { 569 $DB->get_manager()->reset_sequence($table); 570 } else if ($nextid == $current[$table]) { 571 continue; 572 } 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"); 578 } 579 } 580 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(); 586 } 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; 591 } 592 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { 593 $DB->get_manager()->reset_sequence($table); 594 } 595 } 596 } 597 } 598 599 /** 600 * Reset all database tables to default values. 601 * @static 602 * @return bool true if reset done, false if skipped 603 */ 604 public static function reset_database() { 605 global $DB; 606 607 $tables = $DB->get_tables(false); 608 if (!$tables or empty($tables['config'])) { 609 // not installed yet 610 return false; 611 } 612 613 if (!$data = self::get_tabledata()) { 614 // not initialised yet 615 return false; 616 } 617 if (!$structure = self::get_tablestructure()) { 618 // not initialised yet 619 return false; 620 } 621 622 $empties = array(); 623 // Use local copy of self::$tableupdated, as list gets updated in for loop. 624 $updatedtables = self::$tableupdated; 625 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(); 630 } 631 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); 639 } 640 $updatedtables = self::$tableupdated; 641 } 642 643 foreach ($data as $table => $records) { 644 // If table is not modified then no need to do anything. 645 // $updatedtables tables is set after the first run, so check before checking for specific table update. 646 if (!empty($updatedtables) && !isset($updatedtables[$table])) { 647 continue; 648 } 649 650 if (empty($records)) { 651 if (!isset($empties[$table])) { 652 // Table has been modified and is not empty. 653 $DB->delete_records($table, array()); 654 } 655 continue; 656 } 657 658 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) { 659 $currentrecords = $DB->get_records($table, array(), 'id ASC'); 660 $changed = false; 661 foreach ($records as $id => $record) { 662 if (!isset($currentrecords[$id])) { 663 $changed = true; 664 break; 665 } 666 if ((array)$record != (array)$currentrecords[$id]) { 667 $changed = true; 668 break; 669 } 670 unset($currentrecords[$id]); 671 } 672 if (!$changed) { 673 if ($currentrecords) { 674 $lastrecord = end($records); 675 $DB->delete_records_select($table, "id > ?", array($lastrecord->id)); 676 continue; 677 } else { 678 continue; 679 } 680 } 681 } 682 683 $DB->delete_records($table, array()); 684 foreach ($records as $record) { 685 $DB->import_record($table, $record, false, true); 686 } 687 } 688 689 // reset all next record ids - aka sequences 690 self::reset_all_database_sequences($empties); 691 692 // remove extra tables 693 foreach ($tables as $table) { 694 if (!isset($data[$table])) { 695 $DB->get_manager()->drop_table(new xmldb_table($table)); 696 } 697 } 698 699 self::reset_updated_table_list(); 700 701 return true; 702 } 703 704 /** 705 * Purge dataroot directory 706 * @static 707 * @return void 708 */ 709 public static function reset_dataroot() { 710 global $CFG; 711 712 $childclassname = self::get_framework() . '_util'; 713 714 // Do not delete automatically installed files. 715 self::skip_original_data_files($childclassname); 716 717 // Clear file status cache, before checking file_exists. 718 clearstatcache(); 719 720 // Clean up the dataroot folder. 721 $handle = opendir(self::get_dataroot()); 722 while (false !== ($item = readdir($handle))) { 723 if (in_array($item, $childclassname::$datarootskiponreset)) { 724 continue; 725 } 726 if (is_dir(self::get_dataroot()."/$item")) { 727 remove_dir(self::get_dataroot()."/$item", false); 728 } else { 729 unlink(self::get_dataroot()."/$item"); 730 } 731 } 732 closedir($handle); 733 734 // Clean up the dataroot/filedir folder. 735 if (file_exists(self::get_dataroot() . '/filedir')) { 736 $handle = opendir(self::get_dataroot() . '/filedir'); 737 while (false !== ($item = readdir($handle))) { 738 if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) { 739 continue; 740 } 741 if (is_dir(self::get_dataroot()."/filedir/$item")) { 742 remove_dir(self::get_dataroot()."/filedir/$item", false); 743 } else { 744 unlink(self::get_dataroot()."/filedir/$item"); 745 } 746 } 747 closedir($handle); 748 } 749 750 make_temp_directory(''); 751 make_backup_temp_directory(''); 752 make_cache_directory(''); 753 make_localcache_directory(''); 754 // Purge all data from the caches. This is required for consistency between tests. 755 // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache) 756 // and now we will purge any other caches as well. This must be done before the cache_factory::reset() as that 757 // removes all definitions of caches and purge does not have valid caches to operate on. 758 cache_helper::purge_all(); 759 // Reset the cache API so that it recreates it's required directories as well. 760 cache_factory::reset(); 761 } 762 763 /** 764 * Gets a text-based site version description. 765 * 766 * @return string The site info 767 */ 768 public static function get_site_info() { 769 global $CFG; 770 771 $output = ''; 772 773 // All developers have to understand English, do not localise! 774 $env = self::get_environment(); 775 776 $output .= "Moodle ".$env['moodleversion']; 777 if ($hash = self::get_git_hash()) { 778 $output .= ", $hash"; 779 } 780 $output .= "\n"; 781 782 // Add php version. 783 require_once($CFG->libdir.'/environmentlib.php'); 784 $output .= "Php: ". normalize_version($env['phpversion']); 785 786 // Add database type and version. 787 $output .= ", " . $env['dbtype'] . ": " . $env['dbversion']; 788 789 // OS details. 790 $output .= ", OS: " . $env['os'] . "\n"; 791 792 return $output; 793 } 794 795 /** 796 * Try to get current git hash of the Moodle in $CFG->dirroot. 797 * @return string null if unknown, sha1 hash if known 798 */ 799 public static function get_git_hash() { 800 global $CFG; 801 802 // This is a bit naive, but it should mostly work for all platforms. 803 804 if (!file_exists("$CFG->dirroot/.git/HEAD")) { 805 return null; 806 } 807 808 $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD"); 809 if ($headcontent === false) { 810 return null; 811 } 812 813 $headcontent = trim($headcontent); 814 815 // If it is pointing to a hash we return it directly. 816 if (strlen($headcontent) === 40) { 817 return $headcontent; 818 } 819 820 if (strpos($headcontent, 'ref: ') !== 0) { 821 return null; 822 } 823 824 $ref = substr($headcontent, 5); 825 826 if (!file_exists("$CFG->dirroot/.git/$ref")) { 827 return null; 828 } 829 830 $hash = file_get_contents("$CFG->dirroot/.git/$ref"); 831 832 if ($hash === false) { 833 return null; 834 } 835 836 $hash = trim($hash); 837 838 if (strlen($hash) != 40) { 839 return null; 840 } 841 842 return $hash; 843 } 844 845 /** 846 * Set state of modified tables. 847 * 848 * @param string $sql sql which is updating the table. 849 */ 850 public static function set_table_modified_by_sql($sql) { 851 global $DB; 852 853 $prefix = $DB->get_prefix(); 854 855 preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches); 856 // Ignore random sql for testing like "XXUPDATE SET XSSD". 857 if (!empty($matches[1])) { 858 $table = trim($matches[1]); 859 $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); 860 self::$tableupdated[$table] = true; 861 862 if (defined('BEHAT_SITE_RUNNING')) { 863 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path(); 864 $tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true); 865 if (!isset($tablesupdated[$table])) { 866 $tablesupdated[$table] = true; 867 @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT)); 868 } 869 } 870 } 871 } 872 873 /** 874 * Reset updated table list. This should be done after every reset. 875 */ 876 public static function reset_updated_table_list() { 877 self::$tableupdated = array(); 878 } 879 880 /** 881 * Delete tablesupdatedbyscenario file. This should be called before suite, 882 * to ensure full db reset. 883 */ 884 public static function clean_tables_updated_by_scenario_list() { 885 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path(); 886 if (file_exists($tablesupdatedfile)) { 887 unlink($tablesupdatedfile); 888 } 889 890 // Reset static cache of cli process. 891 self::reset_updated_table_list(); 892 } 893 894 /** 895 * Returns the path to the file which holds list of tables updated in scenario. 896 * @return string 897 */ 898 protected final static function get_tables_updated_by_scenario_list_path() { 899 return self::get_dataroot() . '/tablesupdatedbyscenario.json'; 900 } 901 902 /** 903 * Drop the whole test database 904 * @static 905 * @param bool $displayprogress 906 */ 907 protected static function drop_database($displayprogress = false) { 908 global $DB; 909 910 $tables = $DB->get_tables(false); 911 if (isset($tables['config'])) { 912 // config always last to prevent problems with interrupted drops! 913 unset($tables['config']); 914 $tables['config'] = 'config'; 915 } 916 917 if ($displayprogress) { 918 echo "Dropping tables:\n"; 919 } 920 $dotsonline = 0; 921 foreach ($tables as $tablename) { 922 $table = new xmldb_table($tablename); 923 $DB->get_manager()->drop_table($table); 924 925 if ($dotsonline == 60) { 926 if ($displayprogress) { 927 echo "\n"; 928 } 929 $dotsonline = 0; 930 } 931 if ($displayprogress) { 932 echo '.'; 933 } 934 $dotsonline += 1; 935 } 936 if ($displayprogress) { 937 echo "\n"; 938 } 939 } 940 941 /** 942 * Drops the test framework dataroot 943 * @static 944 */ 945 protected static function drop_dataroot() { 946 global $CFG; 947 948 $framework = self::get_framework(); 949 $childclassname = $framework . '_util'; 950 951 $files = scandir(self::get_dataroot() . '/' . $framework); 952 foreach ($files as $file) { 953 if (in_array($file, $childclassname::$datarootskipondrop)) { 954 continue; 955 } 956 $path = self::get_dataroot() . '/' . $framework . '/' . $file; 957 if (is_dir($path)) { 958 remove_dir($path, false); 959 } else { 960 unlink($path); 961 } 962 } 963 964 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; 965 if (file_exists($jsonfilepath)) { 966 // Delete the json file. 967 unlink($jsonfilepath); 968 // Delete the dataroot filedir. 969 remove_dir(self::get_dataroot() . '/filedir', false); 970 } 971 } 972 973 /** 974 * Skip the original dataroot files to not been reset. 975 * 976 * @static 977 * @param string $utilclassname the util class name.. 978 */ 979 protected static function skip_original_data_files($utilclassname) { 980 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; 981 if (file_exists($jsonfilepath)) { 982 983 $listfiles = file_get_contents($jsonfilepath); 984 985 // Mark each files as to not be reset. 986 if (!empty($listfiles) && !self::$originaldatafilesjsonadded) { 987 $originaldatarootfiles = json_decode($listfiles); 988 // Keep the json file. Only drop_dataroot() should delete it. 989 $originaldatarootfiles[] = self::$originaldatafilesjson; 990 $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset, 991 $originaldatarootfiles); 992 self::$originaldatafilesjsonadded = true; 993 } 994 } 995 } 996 997 /** 998 * Save the list of the original dataroot files into a json file. 999 */ 1000 protected static function save_original_data_files() { 1001 global $CFG; 1002 1003 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; 1004 1005 // Save the original dataroot files if not done (only executed the first time). 1006 if (!file_exists($jsonfilepath)) { 1007 1008 $listfiles = array(); 1009 $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.'; 1010 $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..'; 1011 $listfiles[$currentdir] = $currentdir; 1012 $listfiles[$parentdir] = $parentdir; 1013 1014 $filedir = self::get_dataroot() . '/filedir'; 1015 if (file_exists($filedir)) { 1016 $directory = new RecursiveDirectoryIterator($filedir); 1017 foreach (new RecursiveIteratorIterator($directory) as $file) { 1018 if ($file->isDir()) { 1019 $key = substr($file->getPath(), strlen(self::get_dataroot() . '/')); 1020 } else { 1021 $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/')); 1022 } 1023 $listfiles[$key] = $key; 1024 } 1025 } 1026 1027 // Save the file list in a JSON file. 1028 $fp = fopen($jsonfilepath, 'w'); 1029 fwrite($fp, json_encode(array_values($listfiles))); 1030 fclose($fp); 1031 } 1032 } 1033 1034 /** 1035 * Return list of environment versions on which tests will run. 1036 * Environment includes: 1037 * - moodleversion 1038 * - phpversion 1039 * - dbtype 1040 * - dbversion 1041 * - os 1042 * 1043 * @return array 1044 */ 1045 public static function get_environment() { 1046 global $CFG, $DB; 1047 1048 $env = array(); 1049 1050 // Add moodle version. 1051 $release = null; 1052 require("$CFG->dirroot/version.php"); 1053 $env['moodleversion'] = $release; 1054 1055 // Add php version. 1056 $phpversion = phpversion(); 1057 $env['phpversion'] = $phpversion; 1058 1059 // Add database type and version. 1060 $dbtype = $CFG->dbtype; 1061 $dbinfo = $DB->get_server_info(); 1062 $dbversion = $dbinfo['version']; 1063 $env['dbtype'] = $dbtype; 1064 $env['dbversion'] = $dbversion; 1065 1066 // OS details. 1067 $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m'); 1068 $env['os'] = $osdetails; 1069 1070 return $env; 1071 } 1072 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body