Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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 $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! 648 // 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. 651 // 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; 656 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; 661 } 662 } 663 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; 673 } 674 if (!is_null($info->auto_increment)) { 675 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table); 676 $mysqlsequences[$table] = $info->auto_increment; 677 } 678 } 679 $rs->close(); 680 } 681 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; 687 } 688 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); 694 } 695 continue; 696 } 697 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; 703 } 704 } 705 } 706 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); 711 } 712 continue; 713 } 714 715 if (empty($records)) { 716 if (!isset($empties[$table])) { 717 // Table has been modified and is not empty. 718 $DB->delete_records($table, array()); 719 } 720 continue; 721 } 722 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; 730 } 731 if ((array)$record != (array)$currentrecords[$id]) { 732 $changed = true; 733 break; 734 } 735 unset($currentrecords[$id]); 736 } 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; 744 } 745 } 746 } 747 748 $DB->delete_records($table, array()); 749 foreach ($records as $record) { 750 $DB->import_record($table, $record, false, true); 751 } 752 } 753 754 // reset all next record ids - aka sequences 755 self::reset_all_database_sequences($empties); 756 757 // remove extra tables 758 foreach ($tables as $table) { 759 if (!isset($data[$table])) { 760 $DB->get_manager()->drop_table(new xmldb_table($table)); 761 } 762 } 763 764 self::reset_updated_table_list(); 765 766 return true; 767 } 768 769 /** 770 * Purge dataroot directory 771 * @static 772 * @return void 773 */ 774 public static function reset_dataroot() { 775 global $CFG; 776 777 $childclassname = self::get_framework() . '_util'; 778 779 // Do not delete automatically installed files. 780 self::skip_original_data_files($childclassname); 781 782 // Clear file status cache, before checking file_exists. 783 clearstatcache(); 784 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; 790 } 791 if (is_dir(self::get_dataroot()."/$item")) { 792 remove_dir(self::get_dataroot()."/$item", false); 793 } else { 794 unlink(self::get_dataroot()."/$item"); 795 } 796 } 797 closedir($handle); 798 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; 805 } 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"); 810 } 811 } 812 closedir($handle); 813 } 814 815 make_temp_directory(''); 816 make_backup_temp_directory(''); 817 make_cache_directory(''); 818 make_localcache_directory(''); 819 // Purge all data from the caches. This is required for consistency between tests. 820 // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache) 821 // and now we will purge any other caches as well. This must be done before the cache_factory::reset() as that 822 // removes all definitions of caches and purge does not have valid caches to operate on. 823 cache_helper::purge_all(); 824 // Reset the cache API so that it recreates it's required directories as well. 825 cache_factory::reset(); 826 } 827 828 /** 829 * Gets a text-based site version description. 830 * 831 * @return string The site info 832 */ 833 public static function get_site_info() { 834 global $CFG; 835 836 $output = ''; 837 838 // All developers have to understand English, do not localise! 839 $env = self::get_environment(); 840 841 $output .= "Moodle ".$env['moodleversion']; 842 if ($hash = self::get_git_hash()) { 843 $output .= ", $hash"; 844 } 845 $output .= "\n"; 846 847 // Add php version. 848 require_once($CFG->libdir.'/environmentlib.php'); 849 $output .= "Php: ". normalize_version($env['phpversion']); 850 851 // Add database type and version. 852 $output .= ", " . $env['dbtype'] . ": " . $env['dbversion']; 853 854 // OS details. 855 $output .= ", OS: " . $env['os'] . "\n"; 856 857 return $output; 858 } 859 860 /** 861 * Try to get current git hash of the Moodle in $CFG->dirroot. 862 * @return string null if unknown, sha1 hash if known 863 */ 864 public static function get_git_hash() { 865 global $CFG; 866 867 // This is a bit naive, but it should mostly work for all platforms. 868 869 if (!file_exists("$CFG->dirroot/.git/HEAD")) { 870 return null; 871 } 872 873 $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD"); 874 if ($headcontent === false) { 875 return null; 876 } 877 878 $headcontent = trim($headcontent); 879 880 // If it is pointing to a hash we return it directly. 881 if (strlen($headcontent) === 40) { 882 return $headcontent; 883 } 884 885 if (strpos($headcontent, 'ref: ') !== 0) { 886 return null; 887 } 888 889 $ref = substr($headcontent, 5); 890 891 if (!file_exists("$CFG->dirroot/.git/$ref")) { 892 return null; 893 } 894 895 $hash = file_get_contents("$CFG->dirroot/.git/$ref"); 896 897 if ($hash === false) { 898 return null; 899 } 900 901 $hash = trim($hash); 902 903 if (strlen($hash) != 40) { 904 return null; 905 } 906 907 return $hash; 908 } 909 910 /** 911 * Set state of modified tables. 912 * 913 * @param string $sql sql which is updating the table. 914 */ 915 public static function set_table_modified_by_sql($sql) { 916 global $DB; 917 918 $prefix = $DB->get_prefix(); 919 920 preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches); 921 // Ignore random sql for testing like "XXUPDATE SET XSSD". 922 if (!empty($matches[1])) { 923 $table = trim($matches[1]); 924 $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); 925 self::$tableupdated[$table] = true; 926 927 if (defined('BEHAT_SITE_RUNNING')) { 928 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path(); 929 $tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true); 930 if (!isset($tablesupdated[$table])) { 931 $tablesupdated[$table] = true; 932 @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT)); 933 } 934 } 935 } 936 } 937 938 /** 939 * Reset updated table list. This should be done after every reset. 940 */ 941 public static function reset_updated_table_list() { 942 self::$tableupdated = array(); 943 } 944 945 /** 946 * Delete tablesupdatedbyscenario file. This should be called before suite, 947 * to ensure full db reset. 948 */ 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); 953 } 954 955 // Reset static cache of cli process. 956 self::reset_updated_table_list(); 957 } 958 959 /** 960 * Returns the path to the file which holds list of tables updated in scenario. 961 * @return string 962 */ 963 protected final static function get_tables_updated_by_scenario_list_path() { 964 return self::get_dataroot() . '/tablesupdatedbyscenario.json'; 965 } 966 967 /** 968 * Drop the whole test database 969 * @static 970 * @param bool $displayprogress 971 */ 972 protected static function drop_database($displayprogress = false) { 973 global $DB; 974 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'; 980 } 981 982 if ($displayprogress) { 983 echo "Dropping tables:\n"; 984 } 985 $dotsonline = 0; 986 foreach ($tables as $tablename) { 987 $table = new xmldb_table($tablename); 988 $DB->get_manager()->drop_table($table); 989 990 if ($dotsonline == 60) { 991 if ($displayprogress) { 992 echo "\n"; 993 } 994 $dotsonline = 0; 995 } 996 if ($displayprogress) { 997 echo '.'; 998 } 999 $dotsonline += 1; 1000 } 1001 if ($displayprogress) { 1002 echo "\n"; 1003 } 1004 } 1005 1006 /** 1007 * Drops the test framework dataroot 1008 * @static 1009 */ 1010 protected static function drop_dataroot() { 1011 global $CFG; 1012 1013 $framework = self::get_framework(); 1014 $childclassname = $framework . '_util'; 1015 1016 $files = scandir(self::get_dataroot() . '/' . $framework); 1017 foreach ($files as $file) { 1018 if (in_array($file, $childclassname::$datarootskipondrop)) { 1019 continue; 1020 } 1021 $path = self::get_dataroot() . '/' . $framework . '/' . $file; 1022 if (is_dir($path)) { 1023 remove_dir($path, false); 1024 } else { 1025 unlink($path); 1026 } 1027 } 1028 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); 1035 } 1036 } 1037 1038 /** 1039 * Skip the original dataroot files to not been reset. 1040 * 1041 * @static 1042 * @param string $utilclassname the util class name.. 1043 */ 1044 protected static function skip_original_data_files($utilclassname) { 1045 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; 1046 if (file_exists($jsonfilepath)) { 1047 1048 $listfiles = file_get_contents($jsonfilepath); 1049 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; 1058 } 1059 } 1060 } 1061 1062 /** 1063 * Save the list of the original dataroot files into a json file. 1064 */ 1065 protected static function save_original_data_files() { 1066 global $CFG; 1067 1068 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; 1069 1070 // Save the original dataroot files if not done (only executed the first time). 1071 if (!file_exists($jsonfilepath)) { 1072 1073 $listfiles = array(); 1074 $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.'; 1075 $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..'; 1076 $listfiles[$currentdir] = $currentdir; 1077 $listfiles[$parentdir] = $parentdir; 1078 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() . '/')); 1087 } 1088 $listfiles[$key] = $key; 1089 } 1090 } 1091 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); 1096 } 1097 } 1098 1099 /** 1100 * Return list of environment versions on which tests will run. 1101 * Environment includes: 1102 * - moodleversion 1103 * - phpversion 1104 * - dbtype 1105 * - dbversion 1106 * - os 1107 * 1108 * @return array 1109 */ 1110 public static function get_environment() { 1111 global $CFG, $DB; 1112 1113 $env = array(); 1114 1115 // Add moodle version. 1116 $release = null; 1117 require("$CFG->dirroot/version.php"); 1118 $env['moodleversion'] = $release; 1119 1120 // Add php version. 1121 $phpversion = phpversion(); 1122 $env['phpversion'] = $phpversion; 1123 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; 1130 1131 // OS details. 1132 $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m'); 1133 $env['os'] = $osdetails; 1134 1135 return $env; 1136 } 1137 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body