You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1138 lines
39 KiB

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Testing util classes
*
* @abstract
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Utils for test sites creation
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class testing_util {
/**
* @var string dataroot (likely to be $CFG->dataroot).
*/
private static $dataroot = null;
/**
* @var testing_data_generator
*/
protected static $generator = null;
/**
* @var string current version hash from php files
*/
protected static $versionhash = null;
/**
* @var array original content of all database tables
*/
protected static $tabledata = null;
/**
* @var array original structure of all database tables
*/
protected static $tablestructure = null;
/**
* @var array keep list of sequenceid used in a table.
*/
private static $tablesequences = array();
/**
* @var array list of updated tables.
*/
public static $tableupdated = array();
/**
* @var array original structure of all database tables
*/
protected static $sequencenames = null;
/**
* @var string name of the json file where we store the list of dataroot files to not reset during reset_dataroot.
*/
private static $originaldatafilesjson = 'originaldatafiles.json';
/**
* @var boolean set to true once $originaldatafilesjson file is created.
*/
private static $originaldatafilesjsonadded = false;
/**
* @var int next sequence value for a single test cycle.
*/
protected static $sequencenextstartingid = null;
/**
* Return the name of the JSON file containing the init filenames.
*
* @static
* @return string
*/
public static function get_originaldatafilesjson() {
return self::$originaldatafilesjson;
}
/**
* Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
*
* @static
* @return string the dataroot.
*/
public static function get_dataroot() {
global $CFG;
// By default it's the test framework dataroot.
if (empty(self::$dataroot)) {
self::$dataroot = $CFG->dataroot;
}
return self::$dataroot;
}
/**
* Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
*
* @param string $dataroot the dataroot of the test framework.
* @static
*/
public static function set_dataroot($dataroot) {
self::$dataroot = $dataroot;
}
/**
* Returns the testing framework name
* @static
* @return string
*/
protected static final function get_framework() {
$classname = get_called_class();
return substr($classname, 0, strpos($classname, '_'));
}
/**
* Get data generator
* @static
* @return testing_data_generator
*/
public static function get_data_generator() {
if (is_null(self::$generator)) {
require_once(__DIR__.'/../generator/lib.php');
self::$generator = new testing_data_generator();
}
return self::$generator;
}
/**
* Does this site (db and dataroot) appear to be used for production?
* We try very hard to prevent accidental damage done to production servers!!
*
* @static
* @return bool
*/
public static function is_test_site() {
global $DB, $CFG;
$framework = self::get_framework();
if (!file_exists(self::get_dataroot() . '/' . $framework . 'testdir.txt')) {
// this is already tested in bootstrap script,
// but anyway presence of this file means the dataroot is for testing
return false;
}
$tables = $DB->get_tables(false);
if ($tables) {
if (!$DB->get_manager()->table_exists('config')) {
return false;
}
if (!get_config('core', $framework . 'test')) {
return false;
}
}
return true;
}
/**
* Returns whether test database and dataroot were created using the current version codebase
*
* @return bool
*/
public static function is_test_data_updated() {
global $DB;
$framework = self::get_framework();
$datarootpath = self::get_dataroot() . '/' . $framework;
if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
return false;
}
if (!file_exists($datarootpath . '/versionshash.txt')) {
return false;
}
$hash = core_component::get_all_versions_hash();
$oldhash = file_get_contents($datarootpath . '/versionshash.txt');
if ($hash !== $oldhash) {
return false;
}
// A direct database request must be used to avoid any possible caching of an older value.
$dbhash = $DB->get_field('config', 'value', array('name' => $framework . 'test'));
if ($hash !== $dbhash) {
return false;
}
return true;
}
/**
* Stores the status of the database
*
* Serializes the contents and the structure and
* stores it in the test framework space in dataroot
*/
protected static function store_database_state() {
global $DB, $CFG;
$framework = self::get_framework();
// store data for all tables
$data = array();
$structure = array();
$tables = $DB->get_tables();
foreach ($tables as $table) {
$columns = $DB->get_columns($table);
$structure[$table] = $columns;
if (isset($columns['id']) and $columns['id']->auto_increment) {
$data[$table] = $DB->get_records($table, array(), 'id ASC');
} else {
// there should not be many of these
$data[$table] = $DB->get_records($table, array());
}
}
$data = serialize($data);
$datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
file_put_contents($datafile, $data);
testing_fix_file_permissions($datafile);
$structure = serialize($structure);
$structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
file_put_contents($structurefile, $structure);
testing_fix_file_permissions($structurefile);
}
/**
* Stores the version hash in both database and dataroot
*/
protected static function store_versions_hash() {
global $CFG;
$framework = self::get_framework();
$hash = core_component::get_all_versions_hash();
// add test db flag
set_config($framework . 'test', $hash);
// hash all plugin versions - helps with very fast detection of db structure changes
$hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt';
file_put_contents($hashfile, $hash);
testing_fix_file_permissions($hashfile);
}
/**
* Returns contents of all tables right after installation.
* @static
* @return array $table=>$records
*/
protected static function get_tabledata() {
if (!isset(self::$tabledata)) {
$framework = self::get_framework();
$datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
if (!file_exists($datafile)) {
// Not initialised yet.
return array();
}
$data = file_get_contents($datafile);
self::$tabledata = unserialize($data);
}
if (!is_array(self::$tabledata)) {
testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
}
return self::$tabledata;
}
/**
* Returns structure of all tables right after installation.
* @static
* @return array $table=>$records
*/
public static function get_tablestructure() {
if (!isset(self::$tablestructure)) {
$framework = self::get_framework();
$structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
if (!file_exists($structurefile)) {
// Not initialised yet.
return array();
}
$data = file_get_contents($structurefile);
self::$tablestructure = unserialize($data);
}
if (!is_array(self::$tablestructure)) {
testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
}
return self::$tablestructure;
}
/**
* Returns the names of sequences for each autoincrementing id field in all standard tables.
* @static
* @return array $table=>$sequencename
*/
public static function get_sequencenames() {
global $DB;
if (isset(self::$sequencenames)) {
return self::$sequencenames;
}
if (!$structure = self::get_tablestructure()) {
return array();
}
self::$sequencenames = array();
foreach ($structure as $table => $ignored) {
$name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
if ($name !== false) {
self::$sequencenames[$table] = $name;
}
}
return self::$sequencenames;
}
/**
* Returns list of tables that are unmodified and empty.
*
* @static
* @return array of table names, empty if unknown
*/
protected static function guess_unmodified_empty_tables() {
global $DB;
$dbfamily = $DB->get_dbfamily();
if ($dbfamily === 'mysql') {
$empties = array();
$prefix = $DB->get_prefix();
$rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
foreach ($rs as $info) {
$table = strtolower($info->name);
if (strpos($table, $prefix) !== 0) {
// incorrect table match caused by _
continue;
}
if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
$table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
$empties[$table] = $table;
}
}
$rs->close();
return $empties;
} else if ($dbfamily === 'mssql') {
$empties = array();
$prefix = $DB->get_prefix();
$sql = "SELECT t.name
FROM sys.identity_columns i
JOIN sys.tables t ON t.object_id = i.object_id
WHERE t.name LIKE ?
AND i.name = 'id'
AND i.last_value IS NULL";
$rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
foreach ($rs as $info) {
$table = strtolower($info->name);
if (strpos($table, $prefix) !== 0) {
// incorrect table match caused by _
continue;
}
$table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
$empties[$table] = $table;
}
$rs->close();
return $empties;
} else if ($dbfamily === 'oracle') {
$sequences = self::get_sequencenames();
$sequences = array_map('strtoupper', $sequences);
$lookup = array_flip($sequences);
$empties = array();
list($seqs, $params) = $DB->get_in_or_equal($sequences);
$sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $seq) {
$table = $lookup[$seq->sequence_name];
$empties[$table] = $table;
}
$rs->close();
return $empties;
} else {
return array();
}
}
/**
* Determine the next unique starting id sequences.
*
* @static
* @param array $records The records to use to determine the starting value for the table.
* @param string $table table name.
* @return int The value the sequence should be set to.
*/
private static function get_next_sequence_starting_value($records, $table) {
if (isset(self::$tablesequences[$table])) {
return self::$tablesequences[$table];
}
$id = self::$sequencenextstartingid;
// If there are records, calculate the minimum id we can use.
// It must be bigger than the last record's id.
if (!empty($records)) {
$lastrecord = end($records);
$id = max($id, $lastrecord->id + 1);
}
self::$sequencenextstartingid = $id + 1000;
self::$tablesequences[$table] = $id;
return $id;
}
/**
* Reset all database sequences to initial values.
*
* @static
* @param array $empties tables that are known to be unmodified and empty
* @return void
*/
public static function reset_all_database_sequences(array $empties = null) {
global $DB;
if (!$data = self::get_tabledata()) {
// Not initialised yet.
return;
}
if (!$structure = self::get_tablestructure()) {
// Not initialised yet.
return;
}
$updatedtables = self::$tableupdated;
// If all starting Id's are the same, it's difficult to detect coding and testing
// errors that use the incorrect id in tests. The classic case is cmid vs instance id.
// To reduce the chance of the coding error, we start sequences at different values where possible.
// In a attempt to avoid tables with existing id's we start at a high number.
// Reset the value each time all database sequences are reset.
if (defined('PHPUNIT_SEQUENCE_START') and PHPUNIT_SEQUENCE_START) {
self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
} else {
self::$sequencenextstartingid = 100000;
}
$dbfamily = $DB->get_dbfamily();
if ($dbfamily === 'postgres') {
$queries = array();
$prefix = $DB->get_prefix();
foreach ($data as $table => $records) {
// If table is not modified then no need to do anything.
if (!isset($updatedtables[$table])) {
continue;
}
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
$nextid = self::get_next_sequence_starting_value($records, $table);
$queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
}
}
if ($queries) {
$DB->change_database_structure(implode(';', $queries));
}
} else if ($dbfamily === 'mysql') {
$queries = array();
$sequences = array();
$prefix = $DB->get_prefix();
$rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
foreach ($rs as $info) {
$table = strtolower($info->name);
if (strpos($table, $prefix) !== 0) {
// incorrect table match caused by _
continue;
}
if (!is_null($info->auto_increment)) {
$table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
$sequences[$table] = $info->auto_increment;
}
}
$rs->close();
$prefix = $DB->get_prefix();
foreach ($data as $table => $records) {
// If table is not modified then no need to do anything.
if (!isset($updatedtables[$table])) {
continue;
}
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
if (isset($sequences[$table])) {
$nextid = self::get_next_sequence_starting_value($records, $table);
if ($sequences[$table] != $nextid) {
$queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
}
} else {
// some problem exists, fallback to standard code
$DB->get_manager()->reset_sequence($table);
}
}
}
if ($queries) {
$DB->change_database_structure(implode(';', $queries));
}
} else if ($dbfamily === 'oracle') {
$sequences = self::get_sequencenames();
$sequences = array_map('strtoupper', $sequences);
$lookup = array_flip($sequences);
$current = array();
list($seqs, $params) = $DB->get_in_or_equal($sequences);
$sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $seq) {
$table = $lookup[$seq->sequence_name];
$current[$table] = $seq->last_number;
}
$rs->close();
foreach ($data as $table => $records) {
// If table is not modified then no need to do anything.
if (!isset($updatedtables[$table])) {
continue;
}
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
$lastrecord = end($records);
if ($lastrecord) {
$nextid = $lastrecord->id + 1;
} else {
$nextid = 1;
}
if (!isset($current[$table])) {
$DB->get_manager()->reset_sequence($table);
} else if ($nextid == $current[$table]) {
continue;
}
// reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
$seqname = $sequences[$table];
$cachesize = $DB->get_manager()->generator->sequence_cache_size;
$DB->change_database_structure("DROP SEQUENCE $seqname");
$DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
}
}
} else {
// note: does mssql support any kind of faster reset?
// This also implies mssql will not use unique sequence values.
if (is_null($empties) and (empty($updatedtables))) {
$empties = self::guess_unmodified_empty_tables();
}
foreach ($data as $table => $records) {
// If table is not modified then no need to do anything.
if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
continue;
}
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
$DB->get_manager()->reset_sequence($table);
}
}
}
}
/**
* Reset all database tables to default values.
* @static
* @return bool true if reset done, false if skipped
*/
public static function reset_database() {
global $DB;
$tables = $DB->get_tables(false);
if (!$tables or empty($tables['config'])) {
// not installed yet
return false;
}
if (!$data = self::get_tabledata()) {
// not initialised yet
return false;
}
if (!$structure = self::get_tablestructure()) {
// not initialised yet
return false;
}
$empties = array();
// Use local copy of self::$tableupdated, as list gets updated in for loop.
$updatedtables = self::$tableupdated;
// If empty tablesequences list then it's the very first run.
if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
// Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
$empties = self::guess_unmodified_empty_tables();
}
// Check if any table has been modified by behat selenium process.
if (defined('BEHAT_SITE_RUNNING')) {
// Crazy way to reset :(.
$tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
unlink($tablesupdatedfile);
}
$updatedtables = self::$tableupdated;
}
$borkedmysql = false;
if ($DB->get_dbfamily() === 'mysql') {
$version = $DB->get_server_info();
if (version_compare($version['version'], '5.6.0') == 1 and version_compare($version['version'], '5.6.16') == -1) {
// Everything that comes from Oracle is evil!
//
// See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
// You cannot reset the counter to a value less than or equal to to the value that is currently in use.
//
// From 5.6.16 release notes:
// InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
// (Bug #17250787, Bug #69882)
$borkedmysql = true;
} else if (version_compare($version['version'], '10.0.0') == 1) {
// And MariaDB is no better!
// Let's hope they pick the patch sometime later...
$borkedmysql = true;
}
}
if ($borkedmysql) {
$mysqlsequences = array();
$prefix = $DB->get_prefix();
$rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
foreach ($rs as $info) {
$table = strtolower($info->name);
if (strpos($table, $prefix) !== 0) {
// Incorrect table match caused by _ char.
continue;
}
if (!is_null($info->auto_increment)) {
$table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
$mysqlsequences[$table] = $info->auto_increment;
}
}
$rs->close();
}
foreach ($data as $table => $records) {
// If table is not modified then no need to do anything.
// $updatedtables tables is set after the first run, so check before checking for specific table update.
if (!empty($updatedtables) && !isset($updatedtables[$table])) {
continue;
}
if ($borkedmysql) {
if (empty($records)) {
if (!isset($empties[$table])) {
// Table has been modified and is not empty.
$DB->delete_records($table, null);
}
continue;
}
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
$current = $DB->get_records($table, array(), 'id ASC');
if ($current == $records) {
if (isset($mysqlsequences[$table]) and $mysqlsequences[$table] == $structure[$table]['id']->auto_increment) {
continue;
}
}
}
// Use TRUNCATE as a workaround and reinsert everything.
$DB->delete_records($table, null);
foreach ($records as $record) {
$DB->import_record($table, $record, false, true);
}
continue;
}
if (empty($records)) {
if (!isset($empties[$table])) {
// Table has been modified and is not empty.
$DB->delete_records($table, array());
}
continue;
}
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
$currentrecords = $DB->get_records($table, array(), 'id ASC');
$changed = false;
foreach ($records as $id => $record) {
if (!isset($currentrecords[$id])) {
$changed = true;
break;
}
if ((array)$record != (array)$currentrecords[$id]) {
$changed = true;
break;
}
unset($currentrecords[$id]);
}
if (!$changed) {
if ($currentrecords) {
$lastrecord = end($records);
$DB->delete_records_select($table, "id > ?", array($lastrecord->id));
continue;
} else {
continue;
}
}
}
$DB->delete_records($table, array());
foreach ($records as $record) {
$DB->import_record($table, $record, false, true);
}
}
// reset all next record ids - aka sequences
self::reset_all_database_sequences($empties);
// remove extra tables
foreach ($tables as $table) {
if (!isset($data[$table])) {
$DB->get_manager()->drop_table(new xmldb_table($table));
}
}
self::reset_updated_table_list();
return true;
}
/**
* Purge dataroot directory
* @static
* @return void
*/
public static function reset_dataroot() {
global $CFG;
$childclassname = self::get_framework() . '_util';
// Do not delete automatically installed files.
self::skip_original_data_files($childclassname);
// Clear file status cache, before checking file_exists.
clearstatcache();
// Clean up the dataroot folder.
$handle = opendir(self::get_dataroot());
while (false !== ($item = readdir($handle))) {
if (in_array($item, $childclassname::$datarootskiponreset)) {
continue;
}
if (is_dir(self::get_dataroot()."/$item")) {
remove_dir(self::get_dataroot()."/$item", false);
} else {
unlink(self::get_dataroot()."/$item");
}
}
closedir($handle);
// Clean up the dataroot/filedir folder.
if (file_exists(self::get_dataroot() . '/filedir')) {
$handle = opendir(self::get_dataroot() . '/filedir');
while (false !== ($item = readdir($handle))) {
if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) {
continue;
}
if (is_dir(self::get_dataroot()."/filedir/$item")) {
remove_dir(self::get_dataroot()."/filedir/$item", false);
} else {
unlink(self::get_dataroot()."/filedir/$item");
}
}
closedir($handle);
}
make_temp_directory('');
make_backup_temp_directory('');
make_cache_directory('');
make_localcache_directory('');
// Purge all data from the caches. This is required for consistency between tests.
// Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
// and now we will purge any other caches as well. This must be done before the cache_factory::reset() as that
// removes all definitions of caches and purge does not have valid caches to operate on.
cache_helper::purge_all();
// Reset the cache API so that it recreates it's required directories as well.
cache_factory::reset();
}
/**
* Gets a text-based site version description.
*
* @return string The site info
*/
public static function get_site_info() {
global $CFG;
$output = '';
// All developers have to understand English, do not localise!
$env = self::get_environment();
$output .= "Moodle ".$env['moodleversion'];
if ($hash = self::get_git_hash()) {
$output .= ", $hash";
}
$output .= "\n";
// Add php version.
require_once($CFG->libdir.'/environmentlib.php');
$output .= "Php: ". normalize_version($env['phpversion']);
// Add database type and version.
$output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
// OS details.
$output .= ", OS: " . $env['os'] . "\n";
return $output;
}
/**
* Try to get current git hash of the Moodle in $CFG->dirroot.
* @return string null if unknown, sha1 hash if known
*/
public static function get_git_hash() {
global $CFG;
// This is a bit naive, but it should mostly work for all platforms.
if (!file_exists("$CFG->dirroot/.git/HEAD")) {
return null;
}
$headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
if ($headcontent === false) {
return null;
}
$headcontent = trim($headcontent);
// If it is pointing to a hash we return it directly.
if (strlen($headcontent) === 40) {
return $headcontent;
}
if (strpos($headcontent, 'ref: ') !== 0) {
return null;
}
$ref = substr($headcontent, 5);
if (!file_exists("$CFG->dirroot/.git/$ref")) {
return null;
}
$hash = file_get_contents("$CFG->dirroot/.git/$ref");
if ($hash === false) {
return null;
}
$hash = trim($hash);
if (strlen($hash) != 40) {
return null;
}
return $hash;
}
/**
* Set state of modified tables.
*
* @param string $sql sql which is updating the table.
*/
public static function set_table_modified_by_sql($sql) {
global $DB;
$prefix = $DB->get_prefix();
preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
// Ignore random sql for testing like "XXUPDATE SET XSSD".
if (!empty($matches[1])) {
$table = trim($matches[1]);
$table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
self::$tableupdated[$table] = true;
if (defined('BEHAT_SITE_RUNNING')) {
$tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
$tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true);
if (!isset($tablesupdated[$table])) {
$tablesupdated[$table] = true;
@file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
}
}
}
}
/**
* Reset updated table list. This should be done after every reset.
*/
public static function reset_updated_table_list() {
self::$tableupdated = array();
}
/**
* Delete tablesupdatedbyscenario file. This should be called before suite,
* to ensure full db reset.
*/
public static function clean_tables_updated_by_scenario_list() {
$tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
if (file_exists($tablesupdatedfile)) {
unlink($tablesupdatedfile);
}
// Reset static cache of cli process.
self::reset_updated_table_list();
}
/**
* Returns the path to the file which holds list of tables updated in scenario.
* @return string
*/
protected final static function get_tables_updated_by_scenario_list_path() {
return self::get_dataroot() . '/tablesupdatedbyscenario.json';
}
/**
* Drop the whole test database
* @static
* @param bool $displayprogress
*/
protected static function drop_database($displayprogress = false) {
global $DB;
$tables = $DB->get_tables(false);
if (isset($tables['config'])) {
// config always last to prevent problems with interrupted drops!
unset($tables['config']);
$tables['config'] = 'config';
}
if ($displayprogress) {
echo "Dropping tables:\n";
}
$dotsonline = 0;
foreach ($tables as $tablename) {
$table = new xmldb_table($tablename);
$DB->get_manager()->drop_table($table);
if ($dotsonline == 60) {
if ($displayprogress) {
echo "\n";
}
$dotsonline = 0;
}
if ($displayprogress) {
echo '.';
}
$dotsonline += 1;
}
if ($displayprogress) {
echo "\n";
}
}
/**
* Drops the test framework dataroot
* @static
*/
protected static function drop_dataroot() {
global $CFG;
$framework = self::get_framework();
$childclassname = $framework . '_util';
$files = scandir(self::get_dataroot() . '/' . $framework);
foreach ($files as $file) {
if (in_array($file, $childclassname::$datarootskipondrop)) {
continue;
}
$path = self::get_dataroot() . '/' . $framework . '/' . $file;
if (is_dir($path)) {
remove_dir($path, false);
} else {
unlink($path);
}
}
$jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
if (file_exists($jsonfilepath)) {
// Delete the json file.
unlink($jsonfilepath);
// Delete the dataroot filedir.
remove_dir(self::get_dataroot() . '/filedir', false);
}
}
/**
* Skip the original dataroot files to not been reset.
*
* @static
* @param string $utilclassname the util class name..
*/
protected static function skip_original_data_files($utilclassname) {
$jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
if (file_exists($jsonfilepath)) {
$listfiles = file_get_contents($jsonfilepath);
// Mark each files as to not be reset.
if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
$originaldatarootfiles = json_decode($listfiles);
// Keep the json file. Only drop_dataroot() should delete it.
$originaldatarootfiles[] = self::$originaldatafilesjson;
$utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
$originaldatarootfiles);
self::$originaldatafilesjsonadded = true;
}
}
}
/**
* Save the list of the original dataroot files into a json file.
*/
protected static function save_original_data_files() {
global $CFG;
$jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
// Save the original dataroot files if not done (only executed the first time).
if (!file_exists($jsonfilepath)) {
$listfiles = array();
$currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.';
$parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..';
$listfiles[$currentdir] = $currentdir;
$listfiles[$parentdir] = $parentdir;
$filedir = self::get_dataroot() . '/filedir';
if (file_exists($filedir)) {
$directory = new RecursiveDirectoryIterator($filedir);
foreach (new RecursiveIteratorIterator($directory) as $file) {
if ($file->isDir()) {
$key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
} else {
$key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
}
$listfiles[$key] = $key;
}
}
// Save the file list in a JSON file.
$fp = fopen($jsonfilepath, 'w');
fwrite($fp, json_encode(array_values($listfiles)));
fclose($fp);
}
}
/**
* Return list of environment versions on which tests will run.
* Environment includes:
* - moodleversion
* - phpversion
* - dbtype
* - dbversion
* - os
*
* @return array
*/
public static function get_environment() {
global $CFG, $DB;
$env = array();
// Add moodle version.
$release = null;
require("$CFG->dirroot/version.php");
$env['moodleversion'] = $release;
// Add php version.
$phpversion = phpversion();
$env['phpversion'] = $phpversion;
// Add database type and version.
$dbtype = $CFG->dbtype;
$dbinfo = $DB->get_server_info();
$dbversion = $dbinfo['version'];
$env['dbtype'] = $dbtype;
$env['dbversion'] = $dbversion;
// OS details.
$osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
$env['os'] = $osdetails;
return $env;
}
}