MDL-37458 testing common methods generalization
authorDavid Monllao <davidm@moodle.com>
Fri, 11 Jan 2013 09:34:24 +0000 (17:34 +0800)
committerDavid Monllao <davidm@moodle.com>
Fri, 18 Jan 2013 01:17:30 +0000 (09:17 +0800)
admin/tool/phpunit/cli/init.php
admin/tool/phpunit/cli/util.php
lib/phpunit/bootstrap.php
lib/phpunit/bootstraplib.php
lib/phpunit/classes/hint_resultprinter.php
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/testing/classes/test_lock.php
lib/testing/classes/util.php [new file with mode: 0644]
lib/testing/lib.php [new file with mode: 0644]

index ca00d56..bf026a4 100644 (file)
@@ -28,6 +28,7 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
 
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
+require_once(__DIR__.'/../../../../lib/testing/lib.php');
 
 echo "Initialising Moodle PHPUnit test environment...\n";
 
index a7552fe..111be5d 100644 (file)
@@ -30,6 +30,7 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
 
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
+require_once(__DIR__.'/../../../../lib/testing/lib.php');
 
 // now get cli options
 list($options, $unrecognized) = cli_get_params(
@@ -110,7 +111,7 @@ Options:
 -h, --help     Print out this help
 
 Example:
-\$ php ".phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/util.php')." --install
+\$ php ".testing_cli_argument_path('/admin/tool/phpunit/cli/util.php')." --install
 ";
     echo $help;
     exit(0);
index 6ead09f..91ea8e6 100644 (file)
@@ -32,6 +32,7 @@ ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
 require_once(__DIR__.'/bootstraplib.php');
+require_once(__DIR__.'/../testing/lib.php');
 
 if (isset($_SERVER['REMOTE_ADDR'])) {
     phpunit_bootstrap_error(1, 'Unit tests can be executed only from command line!');
@@ -130,7 +131,7 @@ if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
     }
 
     // now we are 100% sure this dir is used only for phpunit tests
-    phpunit_bootstrap_initdataroot($CFG->phpunit_dataroot);
+    testing_initdataroot($CFG->phpunit_dataroot, 'phpunit');
 }
 
 // verify db prefix
index b7c9fde..28f4345 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * PHPUnit bootstrap function
  *
- * Note: these functions must be self contained and must not rely on any library or include
+ * Note: these functions must be self contained and must not rely on any other library or include
  *
  * @package    core
  * @category   phpunit
@@ -25,6 +25,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+require_once(__DIR__ . '/../testing/lib.php');
+
 define('PHPUNIT_EXITCODE_PHPUNITMISSING', 129);
 define('PHPUNIT_EXITCODE_PHPUNITWRONG', 130);
 define('PHPUNIT_EXITCODE_PHPUNITEXTMISSING', 131);
@@ -63,11 +65,11 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
             $text = "Moodle PHPUnit environment configuration warning:\n".$text;
             break;
         case PHPUNIT_EXITCODE_INSTALL:
-            $path = phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/init.php');
+            $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php');
             $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path";
             break;
         case PHPUNIT_EXITCODE_REINSTALL:
-            $path = phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/init.php');
+            $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php');
             $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path";
             break;
         default:
@@ -76,90 +78,5 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
             break;
     }
 
-    // do not write to error stream because we need the error message in PHP exec result from web ui
-    echo($text."\n");
-    exit($errorcode);
-}
-
-/**
- * Returns relative path against current working directory,
- * to be used for shell execution hints.
- * @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php"
- * @return string path relative to current directory or absolute path
- */
-function phpunit_bootstrap_cli_argument_path($moodlepath) {
-    global $CFG;
-
-    if (isset($CFG->admin) and $CFG->admin !== 'admin') {
-        $moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath);
-    }
-
-    $cwd = getcwd();
-    if (substr($cwd, -1) !== DIRECTORY_SEPARATOR) {
-        $cwd .= DIRECTORY_SEPARATOR;
-    }
-    $path = realpath($CFG->dirroot.$moodlepath);
-
-    if (strpos($path, $cwd) === 0) {
-        $path = substr($path, strlen($cwd));
-    }
-
-    if (phpunit_bootstrap_is_cygwin()) {
-        $path = str_replace('\\', '/', $path);
-    }
-
-    return $path;
-}
-
-/**
- * Mark empty dataroot to be used for testing.
- * @param string $dataroot The dataroot directory
- * @return void
- */
-function phpunit_bootstrap_initdataroot($dataroot) {
-    global $CFG;
-    umask(0);
-    if (!file_exists("$dataroot/phpunittestdir.txt")) {
-        file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!');
-    }
-    phpunit_boostrap_fix_file_permissions("$dataroot/phpunittestdir.txt");
-    if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
-        mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions);
-    }
-}
-
-/**
- * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
- * @param string $file
- * @return bool success
- */
-function phpunit_boostrap_fix_file_permissions($file) {
-    global $CFG;
-
-    $permissions = fileperms($file);
-    if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
-        $permissions = $permissions | $CFG->filepermissions;
-        return chmod($file, $permissions);
-    }
-
-    return true;
-}
-
-/**
- * Find out if running under Cygwin on Windows.
- * @return bool
- */
-function phpunit_bootstrap_is_cygwin() {
-    if (empty($_SERVER['OS']) or $_SERVER['OS'] !== 'Windows_NT') {
-        return false;
-
-    } else if (!empty($_SERVER['SHELL']) and $_SERVER['SHELL'] === '/bin/bash') {
-        return true;
-
-    } else if (!empty($_SERVER['TERM']) and $_SERVER['TERM'] === 'cygwin') {
-        return true;
-
-    } else {
-        return false;
-    }
+    testing_error($errorcode, $text);
 }
index 4f68206..a04b326 100644 (file)
@@ -106,7 +106,7 @@ class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
 
         if (!$executable) {
             $executable = 'phpunit';
-            if (phpunit_bootstrap_is_cygwin()) {
+            if (testing_is_cygwin()) {
                 $file = str_replace('\\', '/', $file);
                 $executable = 'phpunit.bat';
             }
index f08f490..3dcf545 100644 (file)
@@ -23,6 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+require_once(__DIR__.'/../../testing/classes/util.php');
 
 /**
  * Collection of utility methods.
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class phpunit_util {
-    /** @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 original structure of all database tables */
-    protected static $sequencenames = null;
-
+class phpunit_util extends testing_util {
     /** @var array An array of original globals, restored after each test */
     protected static $globals = array();
 
-    /** @var int last value of db writes counter, used for db resetting */
-    public static $lastdbwrites = null;
-
-    /** @var testing_data_generator */
-    protected static $generator = null;
-
     /** @var array list of debugging messages triggered during the last test execution */
     protected static $debuggings = array();
 
@@ -61,27 +44,14 @@ class phpunit_util {
     protected static $messagesink = null;
 
     /**
-     * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
-     *
-     * Note: do not call manually!
-     *
-     * @internal
-     * @static
-     * @return void
+     * @var array Files to skip when resetting dataroot folder
      */
-    public static function acquire_test_lock() {
-        test_lock::acquire('phpunit');
-    }
+    protected static $datarootskiponreset = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
 
     /**
-     * Note: do not call manually!
-     * @internal
-     * @static
-     * @return void
+     * @var array Files to skip when dropping dataroot folder
      */
-    public static function release_test_lock() {
-        test_lock::release('phpunit');
-    }
+    protected static $datarootskipondrop = array('.', '..', 'lock', 'webrunner.xml');
 
     /**
      * Load global $CFG;
@@ -99,7 +69,7 @@ class phpunit_util {
             initialise_cfg();
             return;
         }
-        if ($dbhash !== phpunit_util::get_version_hash()) {
+        if ($dbhash !== self::get_version_hash()) {
             // do not set CFG - the only way forward is to drop and reinstall
             return;
         }
@@ -107,413 +77,6 @@ class phpunit_util {
         initialise_cfg();
     }
 
-    /**
-     * Get data generator
-     * @static
-     * @return testing_data_generator
-     */
-    public static function get_data_generator() {
-        if (is_null(self::$generator)) {
-            require_once(__DIR__.'/../../testing/generator/lib.php');
-            self::$generator = new testing_data_generator();
-        }
-        return self::$generator;
-    }
-
-    /**
-     * Returns contents of all tables right after installation.
-     * @static
-     * @return array $table=>$records
-     */
-    protected static function get_tabledata() {
-        global $CFG;
-
-        if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
-            // not initialised yet
-            return array();
-        }
-
-        if (!isset(self::$tabledata)) {
-            $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
-            self::$tabledata = unserialize($data);
-        }
-
-        if (!is_array(self::$tabledata)) {
-            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/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() {
-        global $CFG;
-
-        if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
-            // not initialised yet
-            return array();
-        }
-
-        if (!isset(self::$tablestructure)) {
-            $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
-            self::$tablestructure = unserialize($data);
-        }
-
-        if (!is_array(self::$tablestructure)) {
-            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/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)) {
-                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
-                    if ($info->auto_increment == 1) {
-                        $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 = phpunit_util::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();
-        }
-    }
-
-    /**
-     * 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;
-        }
-
-        $dbfamily = $DB->get_dbfamily();
-        if ($dbfamily === 'postgres') {
-            $queries = array();
-            $prefix = $DB->get_prefix();
-            foreach ($data as $table=>$records) {
-                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    if (empty($records)) {
-                        $nextid = 1;
-                    } else {
-                        $lastrecord = end($records);
-                        $nextid = $lastrecord->id + 1;
-                    }
-                    $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
-                }
-            }
-            if ($queries) {
-                $DB->change_database_structure(implode(';', $queries));
-            }
-
-        } else if ($dbfamily === 'mysql') {
-            $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 (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    if (isset($sequences[$table])) {
-                        if (empty($records)) {
-                            $nextid = 1;
-                        } else {
-                            $lastrecord = end($records);
-                            $nextid = $lastrecord->id + 1;
-                        }
-                        if ($sequences[$table] != $nextid) {
-                            $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
-                        }
-
-                    } else {
-                        // some problem exists, fallback to standard code
-                        $DB->get_manager()->reset_sequence($table);
-                    }
-                }
-            }
-
-        } else if ($dbfamily === 'oracle') {
-            $sequences = phpunit_util::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 (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?
-            if (is_null($empties)) {
-                $empties = self::guess_unmodified_empty_tables();
-            }
-            foreach ($data as $table=>$records) {
-                if (isset($empties[$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;
-
-        if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
-            return false;
-        }
-
-        $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 = self::guess_unmodified_empty_tables();
-
-        foreach ($data as $table=>$records) {
-            if (empty($records)) {
-                if (isset($empties[$table])) {
-                    // table was not modified and is empty
-                } else {
-                    $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::$lastdbwrites = $DB->perf_get_writes();
-
-        return true;
-    }
-
-    /**
-     * Purge dataroot directory
-     * @static
-     * @return void
-     */
-    public static function reset_dataroot() {
-        global $CFG;
-
-        $handle = opendir($CFG->dataroot);
-        $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
-        while (false !== ($item = readdir($handle))) {
-            if (in_array($item, $skip)) {
-                continue;
-            }
-            if (is_dir("$CFG->dataroot/$item")) {
-                remove_dir("$CFG->dataroot/$item", false);
-            } else {
-                unlink("$CFG->dataroot/$item");
-            }
-        }
-        closedir($handle);
-        make_temp_directory('');
-        make_cache_directory('');
-        make_cache_directory('htmlpurifier');
-        // Reset the cache API so that it recreates it's required directories as well.
-        cache_factory::reset();
-        // Purge all data from the caches. This is required for consistency.
-        // 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.
-        cache_helper::purge_all();
-    }
-
     /**
      * Reset contents of all database tables to initial values, reset caches, etc.
      *
@@ -695,35 +258,6 @@ class phpunit_util {
         return null;
     }
 
-    /**
-     * 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;
-
-        if (!file_exists("$CFG->dataroot/phpunittestdir.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', 'phpunittest')) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
     /**
      * Is this site initialised to run unit tests?
      *
@@ -731,36 +265,19 @@ class phpunit_util {
      * @return int array errorcode=>message, 0 means ok
      */
     public static function testing_ready_problem() {
-        global $CFG, $DB;
-
-        $tables = $DB->get_tables(false);
+        global $DB;
 
         if (!self::is_test_site()) {
             // dataroot was verified in bootstrap, so it must be DB
             return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
         }
 
+        $tables = $DB->get_tables(false);
         if (empty($tables)) {
             return array(PHPUNIT_EXITCODE_INSTALL, '');
         }
 
-        if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
-            return array(PHPUNIT_EXITCODE_REINSTALL, '');
-        }
-
-        if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
-            return array(PHPUNIT_EXITCODE_REINSTALL, '');
-        }
-
-        $hash = phpunit_util::get_version_hash();
-        $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
-
-        if ($hash !== $oldhash) {
-            return array(PHPUNIT_EXITCODE_REINSTALL, '');
-        }
-
-        $dbhash = get_config('core', 'phpunittest');
-        if ($hash !== $dbhash) {
+        if (!self::is_test_data_updated()) {
             return array(PHPUNIT_EXITCODE_REINSTALL, '');
         }
 
@@ -787,52 +304,13 @@ class phpunit_util {
         if ($displayprogress) {
             echo "Purging dataroot:\n";
         }
+
         self::reset_dataroot();
-        phpunit_bootstrap_initdataroot($CFG->dataroot);
-        $keep = array('.', '..', 'lock', 'webrunner.xml');
-        $files = scandir("$CFG->dataroot/phpunit");
-        foreach ($files as $file) {
-            if (in_array($file, $keep)) {
-                continue;
-            }
-            $path = "$CFG->dataroot/phpunit/$file";
-            if (is_dir($path)) {
-                remove_dir($path, false);
-            } else {
-                unlink($path);
-            }
-        }
+        testing_initdataroot($CFG->dataroot, 'phpunit');
+        self::drop_dataroot();
 
         // drop all tables
-        $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";
-        }
+        self::drop_database($displayprogress);
     }
 
     /**
@@ -870,84 +348,11 @@ class phpunit_util {
         $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
         update_timezone_records($timezones);
 
-        // add test db flag
-        $hash = phpunit_util::get_version_hash();
-        set_config('phpunittest', $hash);
-
-        // 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);
-        file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
-
-        $structure = serialize($structure);
-        file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
-
-        // hash all plugin versions - helps with very fast detection of db structure changes
-        file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
-    }
-
-    /**
-     * Calculate unique version hash for all plugins and core.
-     * @static
-     * @return string sha1 hash
-     */
-    public static function get_version_hash() {
-        global $CFG;
-
-        if (self::$versionhash) {
-            return self::$versionhash;
-        }
-
-        $versions = array();
-
-        // main version first
-        $version = null;
-        include($CFG->dirroot.'/version.php');
-        $versions['core'] = $version;
-
-        // modules
-        $mods = get_plugin_list('mod');
-        ksort($mods);
-        foreach ($mods as $mod => $fullmod) {
-            $module = new stdClass();
-            $module->version = null;
-            include($fullmod.'/version.php');
-            $versions[$mod] = $module->version;
-        }
-
-        // now the rest of plugins
-        $plugintypes = get_plugin_types();
-        unset($plugintypes['mod']);
-        ksort($plugintypes);
-        foreach ($plugintypes as $type=>$unused) {
-            $plugs = get_plugin_list($type);
-            ksort($plugs);
-            foreach ($plugs as $plug=>$fullplug) {
-                $plugin = new stdClass();
-                $plugin->version = null;
-                @include($fullplug.'/version.php');
-                $versions[$plug] = $plugin->version;
-            }
-        }
-
-        self::$versionhash = sha1(serialize($versions));
+        // Store version hash in the database and in a file.
+        self::store_versions_hash();
 
-        return self::$versionhash;
+        // Store database data and structure.
+        self::store_database_state();
     }
 
     /**
@@ -991,7 +396,7 @@ class phpunit_util {
         $result = false;
         if (is_writable($CFG->dirroot)) {
             if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
-                phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
+                testing_fix_file_permissions("$CFG->dirroot/phpunit.xml");
             }
         }
 
@@ -1001,7 +406,7 @@ class phpunit_util {
             '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
             $data);
         file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
+        testing_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
 
         return (bool)$result;
     }
@@ -1046,7 +451,7 @@ class phpunit_util {
             $result = false;
             if (is_writable($cpath)) {
                 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
-                    phpunit_boostrap_fix_file_permissions("$cpath/phpunit.xml");
+                    testing_fix_file_permissions("$cpath/phpunit.xml");
                 }
             }
             // Problems writing file, throw error
index ac30f62..2ceaf3e 100644 (file)
@@ -35,6 +35,6 @@ require_once(__DIR__.'/classes/database_driver_testcase.php');
 require_once(__DIR__.'/classes/arraydataset.php');
 require_once(__DIR__.'/classes/advanced_testcase.php');
 require_once(__DIR__.'/classes/unittestcase.php');
-require_once(__DIR__.'/classes/hint_resultprinter.php'); // Loaded here because phpunit.xml does not support relative links for printerFile
+require_once(__DIR__.'/classes/hint_resultprinter.php'); // Loaded here because phpunit.xml does not support relative links for printerFile.
 require_once(__DIR__.'/../testing/classes/test_lock.php');
 require_once(__DIR__.'/../testing/classes/tests_finder.php');
index b48bfd0..710cdea 100644 (file)
@@ -23,7 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once(__DIR__.'/lib.php');
+require_once(__DIR__.'/../lib.php');
 
 /**
  * Tests lock to prevent concurrent executions of the same test suite
diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php
new file mode 100644 (file)
index 0000000..8588b1b
--- /dev/null
@@ -0,0 +1,727 @@
+<?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 int last value of db writes counter, used for db resetting
+     */
+    public static $lastdbwrites = 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 original structure of all database tables
+     */
+    protected static $sequencenames = null;
+
+    /**
+     * 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($CFG->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 boolean
+     */
+    protected static function is_test_data_updated() {
+        global $CFG;
+
+        $framework = self::get_framework();
+
+        $datarootpath = $CFG->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 = self::get_version_hash();
+        $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
+
+        if ($hash !== $oldhash) {
+            return false;
+        }
+
+        $dbhash = get_config('core', $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 = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
+        file_put_contents($datafile, $data);
+        testing_fix_file_permissions($datafile);
+
+        $structure = serialize($structure);
+        $structurefile = $CFG->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 = self::get_version_hash();
+
+        // add test db flag
+        set_config($framework . 'test', $hash);
+
+        // hash all plugin versions - helps with very fast detection of db structure changes
+        $hashfile = $CFG->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() {
+        global $CFG;
+
+        $framework = self::get_framework();
+
+        $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
+        if (!file_exists($datafile)) {
+            // Not initialised yet.
+            return array();
+        }
+
+        if (!isset(self::$tabledata)) {
+            $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() {
+        global $CFG;
+
+        $framework = self::get_framework();
+
+        $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
+        if (!file_exists($structurefile)) {
+            // Not initialised yet.
+            return array();
+        }
+
+        if (!isset(self::$tablestructure)) {
+            $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)) {
+                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                    if ($info->auto_increment == 1) {
+                        $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();
+        }
+    }
+
+    /**
+     * 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;
+        }
+
+        $dbfamily = $DB->get_dbfamily();
+        if ($dbfamily === 'postgres') {
+            $queries = array();
+            $prefix = $DB->get_prefix();
+            foreach ($data as $table => $records) {
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    if (empty($records)) {
+                        $nextid = 1;
+                    } else {
+                        $lastrecord = end($records);
+                        $nextid = $lastrecord->id + 1;
+                    }
+                    $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
+                }
+            }
+            if ($queries) {
+                $DB->change_database_structure(implode(';', $queries));
+            }
+
+        } else if ($dbfamily === 'mysql') {
+            $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 (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    if (isset($sequences[$table])) {
+                        if (empty($records)) {
+                            $nextid = 1;
+                        } else {
+                            $lastrecord = end($records);
+                            $nextid = $lastrecord->id + 1;
+                        }
+                        if ($sequences[$table] != $nextid) {
+                            $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
+                        }
+
+                    } else {
+                        // some problem exists, fallback to standard code
+                        $DB->get_manager()->reset_sequence($table);
+                    }
+                }
+            }
+
+        } 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 (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?
+            if (is_null($empties)) {
+                $empties = self::guess_unmodified_empty_tables();
+            }
+            foreach ($data as $table => $records) {
+                if (isset($empties[$table])) {
+                    continue;
+                }
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    $DB->get_manager()->reset_sequence($table);
+                }
+            }
+        }
+    }
+
+    /**
+     * Resets the database
+     * @static
+     * @return boolean Returns whether database has been modified or not
+     */
+    public static function reset_database() {
+        global $DB;
+
+        if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
+            return false;
+        }
+
+        $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 = self::guess_unmodified_empty_tables();
+
+        foreach ($data as $table => $records) {
+            if (empty($records)) {
+                if (isset($empties[$table])) {
+                    // table was not modified and is empty
+                } else {
+                    $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::$lastdbwrites = $DB->perf_get_writes();
+
+        return true;
+    }
+
+    /**
+     * Purge dataroot directory
+     * @static
+     * @return void
+     */
+    public static function reset_dataroot() {
+        global $CFG;
+
+        $childclassname = self::get_framework() . '_util';
+
+        $handle = opendir($CFG->dataroot);
+        while (false !== ($item = readdir($handle))) {
+            if (in_array($item, $childclassname::$datarootskiponreset)) {
+                continue;
+            }
+            if (is_dir("$CFG->dataroot/$item")) {
+                remove_dir("$CFG->dataroot/$item", false);
+            } else {
+                unlink("$CFG->dataroot/$item");
+            }
+        }
+        closedir($handle);
+        make_temp_directory('');
+        make_cache_directory('');
+        make_cache_directory('htmlpurifier');
+        // Reset the cache API so that it recreates it's required directories as well.
+        cache_factory::reset();
+        // Purge all data from the caches. This is required for consistency.
+        // 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.
+        cache_helper::purge_all();
+    }
+
+    /**
+     * Drop the whole test database
+     * @static
+     * @param boolean $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($CFG->dataroot . '/' . $framework);
+        foreach ($files as $file) {
+            if (in_array($file, $childclassname::$datarootskipondrop)) {
+                continue;
+            }
+            $path = $CFG->dataroot . '/' . $framework . '/' . $file;
+            if (is_dir($path)) {
+                remove_dir($path, false);
+            } else {
+                unlink($path);
+            }
+        }
+    }
+
+    /**
+     * Reset all database tables to default values.
+     * @static
+     * @return bool true if reset done, false if skipped
+     */
+    /**
+     * Calculate unique version hash for all plugins and core.
+     * @static
+     * @return string sha1 hash
+     */
+    public static function get_version_hash() {
+        global $CFG;
+
+        if (self::$versionhash) {
+            return self::$versionhash;
+        }
+
+        $versions = array();
+
+        // main version first
+        $version = null;
+        include($CFG->dirroot.'/version.php');
+        $versions['core'] = $version;
+
+        // modules
+        $mods = get_plugin_list('mod');
+        ksort($mods);
+        foreach ($mods as $mod => $fullmod) {
+            $module = new stdClass();
+            $module->version = null;
+            include($fullmod.'/version.php');
+            $versions[$mod] = $module->version;
+        }
+
+        // now the rest of plugins
+        $plugintypes = get_plugin_types();
+        unset($plugintypes['mod']);
+        ksort($plugintypes);
+        foreach ($plugintypes as $type => $unused) {
+            $plugs = get_plugin_list($type);
+            ksort($plugs);
+            foreach ($plugs as $plug => $fullplug) {
+                $plugin = new stdClass();
+                $plugin->version = null;
+                @include($fullplug.'/version.php');
+                $versions[$plug] = $plugin->version;
+            }
+        }
+
+        self::$versionhash = sha1(serialize($versions));
+
+        return self::$versionhash;
+    }
+
+}
diff --git a/lib/testing/lib.php b/lib/testing/lib.php
new file mode 100644 (file)
index 0000000..0713d6b
--- /dev/null
@@ -0,0 +1,130 @@
+<?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 general functions
+ *
+ * Note: these functions must be self contained and must not rely on any library or include
+ *
+ * @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
+ */
+
+/**
+ * Returns relative path against current working directory,
+ * to be used for shell execution hints.
+ * @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php"
+ * @return string path relative to current directory or absolute path
+ */
+function testing_cli_argument_path($moodlepath) {
+    global $CFG;
+
+    if (isset($CFG->admin) and $CFG->admin !== 'admin') {
+        $moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath);
+    }
+
+    $cwd = getcwd();
+    if (substr($cwd, -1) !== DIRECTORY_SEPARATOR) {
+        $cwd .= DIRECTORY_SEPARATOR;
+    }
+    $path = realpath($CFG->dirroot.$moodlepath);
+
+    if (strpos($path, $cwd) === 0) {
+        $path = substr($path, strlen($cwd));
+    }
+
+    if (testing_is_cygwin()) {
+        $path = str_replace('\\', '/', $path);
+    }
+
+    return $path;
+}
+
+/**
+ * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
+ * @param string $file
+ * @return bool success
+ */
+function testing_fix_file_permissions($file) {
+    global $CFG;
+
+    $permissions = fileperms($file);
+    if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
+        $permissions = $permissions | $CFG->filepermissions;
+        return chmod($file, $permissions);
+    }
+
+    return true;
+}
+
+/**
+ * Find out if running under Cygwin on Windows.
+ * @return bool
+ */
+function testing_is_cygwin() {
+    if (empty($_SERVER['OS']) or $_SERVER['OS'] !== 'Windows_NT') {
+        return false;
+
+    } else if (!empty($_SERVER['SHELL']) and $_SERVER['SHELL'] === '/bin/bash') {
+        return true;
+
+    } else if (!empty($_SERVER['TERM']) and $_SERVER['TERM'] === 'cygwin') {
+        return true;
+
+    } else {
+        return false;
+    }
+}
+
+/**
+ * Mark empty dataroot to be used for testing.
+ * @param string $dataroot  The dataroot directory
+ * @param string $framework The test framework
+ * @return void
+ */
+function testing_initdataroot($dataroot, $framework) {
+    global $CFG;
+
+    $filename = $dataroot . '/' . $framework . 'testdir.txt';
+
+    umask(0);
+    if (!file_exists($filename)) {
+        file_put_contents($filename, 'Contents of this directory are used during tests only, do not delete this file!');
+    }
+    testing_fix_file_permissions($filename);
+
+    $varname = $framework . '_dataroot';
+    $datarootdir = $CFG->{$varname} . '/' . $framework;
+    if (!file_exists($datarootdir)) {
+        mkdir($datarootdir, $CFG->directorypermissions);
+    }
+}
+
+/**
+ * Prints an error and stops execution
+ *
+ * @param integer $errorcode
+ * @param string $text
+ * @return void exits
+ */
+function testing_error($errorcode, $text = '') {
+
+    // do not write to error stream because we need the error message in PHP exec result from web ui
+    echo($text."\n");
+    exit($errorcode);
+}