From 3f7211f45a9d8df0824184da40e859415f984f42 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Fri, 11 Jan 2013 13:57:19 +0800 Subject: [PATCH 1/3] MDL-37458 testing lock externalization --- admin/tool/phpunit/cli/util.php | 2 +- lib/phpunit/classes/util.php | 34 +---------- lib/phpunit/lib.php | 2 +- lib/setup.php | 2 +- lib/testing/classes/test_lock.php | 98 +++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 35 deletions(-) create mode 100644 lib/testing/classes/test_lock.php diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index 5cfd1285690..a7552fe8e43 100644 --- a/admin/tool/phpunit/cli/util.php +++ b/admin/tool/phpunit/cli/util.php @@ -136,7 +136,7 @@ if ($diag) { } else if ($drop) { // make sure tests do not run in parallel - phpunit_util::acquire_test_lock(); + test_lock::acquire('phpunit'); phpunit_util::drop_site(true); // note: we must stop here because $CFG is messed up and we can not reinstall, sorry exit(0); diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 855269d67c8..92d8c9dfa19 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -54,9 +54,6 @@ class phpunit_util { /** @var testing_data_generator */ protected static $generator = null; - /** @var resource used for prevention of parallel test execution */ - protected static $lockhandle = null; - /** @var array list of debugging messages triggered during the last test execution */ protected static $debuggings = array(); @@ -73,30 +70,7 @@ class phpunit_util { * @return void */ public static function acquire_test_lock() { - global $CFG; - if (!file_exists("$CFG->phpunit_dataroot/phpunit")) { - // dataroot not initialised yet - return; - } - if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) { - file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests'); - phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock"); - } - if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) { - $wouldblock = null; - $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock); - if (!$locked) { - if ($wouldblock) { - echo "Waiting for other test execution to complete...\n"; - } - $locked = flock(self::$lockhandle, LOCK_EX); - } - if (!$locked) { - fclose(self::$lockhandle); - self::$lockhandle = null; - } - } - register_shutdown_function(array('phpunit_util', 'release_test_lock')); + test_lock::acquire('phpunit'); } /** @@ -106,11 +80,7 @@ class phpunit_util { * @return void */ public static function release_test_lock() { - if (self::$lockhandle) { - flock(self::$lockhandle, LOCK_UN); - fclose(self::$lockhandle); - self::$lockhandle = null; - } + test_lock::release('phpunit'); } /** diff --git a/lib/phpunit/lib.php b/lib/phpunit/lib.php index 95b7f73466d..64d72f9e0fd 100644 --- a/lib/phpunit/lib.php +++ b/lib/phpunit/lib.php @@ -36,4 +36,4 @@ 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__.'/../testing/classes/test_lock.php'); diff --git a/lib/setup.php b/lib/setup.php index 3037136f282..84f630fb301 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -488,7 +488,7 @@ setup_DB(); if (PHPUNIT_TEST and !PHPUNIT_UTIL) { // make sure tests do not run in parallel - phpunit_util::acquire_test_lock(); + test_lock::acquire('phpunit'); $dbhash = null; try { if ($dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'))) { diff --git a/lib/testing/classes/test_lock.php b/lib/testing/classes/test_lock.php new file mode 100644 index 00000000000..b48bfd0e163 --- /dev/null +++ b/lib/testing/classes/test_lock.php @@ -0,0 +1,98 @@ +. + +/** + * Tests lock + * + * @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 + */ + +require_once(__DIR__.'/lib.php'); + +/** + * Tests lock to prevent concurrent executions of the same test suite + * + * @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 + */ +class test_lock { + + /** + * @var array Array of resource used for prevention of parallel test execution + */ + protected static $lockhandles = array(); + + /** + * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot. + * + * Note: do not call manually! + * + * @internal + * @static + * @param string $framework Test framework + * @return void + */ + public static function acquire($framework) { + global $CFG; + + $datarootpath = $CFG->{$framework . '_dataroot'} . '/' . $framework; + $lockfile = $datarootpath . '/lock'; + if (!file_exists($datarootpath)) { + // Dataroot not initialised yet. + return; + } + if (!file_exists($lockfile)) { + file_put_contents($lockfile, 'This file prevents concurrent execution of Moodle ' . $framework . ' tests'); + testing_fix_file_permissions($lockfile); + } + if (self::$lockhandles[$framework] = fopen($lockfile, 'r')) { + $wouldblock = null; + $locked = flock(self::$lockhandles[$framework], (LOCK_EX | LOCK_NB), $wouldblock); + if (!$locked) { + if ($wouldblock) { + echo "Waiting for other test execution to complete...\n"; + } + $locked = flock(self::$lockhandles[$framework], LOCK_EX); + } + if (!$locked) { + fclose(self::$lockhandles[$framework]); + self::$lockhandles[$framework] = null; + } + } + register_shutdown_function(array('test_lock', 'release'), $framework); + } + + /** + * Note: do not call manually! + * @internal + * @static + * @param string $framework phpunit|behat + * @return void + */ + public static function release($framework) { + if (self::$lockhandles[$framework]) { + flock(self::$lockhandles[$framework], LOCK_UN); + fclose(self::$lockhandles[$framework]); + self::$lockhandles[$framework] = null; + } + } + +} From f822840223918bca049c29c4d95f9752dde67fae Mon Sep 17 00:00:00 2001 From: David Monllao Date: Mon, 15 Oct 2012 13:00:16 +0800 Subject: [PATCH 2/3] MDL-37458 testing tests finder externalization --- lib/phpunit/classes/util.php | 119 +---------------- lib/phpunit/lib.php | 1 + lib/testing/classes/tests_finder.php | 190 +++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 117 deletions(-) create mode 100644 lib/testing/classes/tests_finder.php diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 92d8c9dfa19..f08f490cf64 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -1026,17 +1026,8 @@ class phpunit_util { $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); $ftemplate = preg_replace('|', $ftemplate); - // Get all the components - $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests(); - - // Get all the directories having tests - $directories = self::get_all_directories_with_tests(); - - // Find any directory not covered by proper components - $remaining = array_diff($directories, $components); - - // Add them to the list of components - $components += $remaining; + // Gets all the components with tests + $components = tests_finder::get_components_with_tests('phpunit'); // Create the corresponding phpunit.xml file for each component foreach ($components as $cname => $cpath) { @@ -1065,112 +1056,6 @@ class phpunit_util { } } - /** - * Returns all the plugins having PHPUnit tests - * - * @return array all the plugins having PHPUnit tests - * - */ - private static function get_all_plugins_with_tests() { - $pluginswithtests = array(); - - $plugintypes = get_plugin_types(); - ksort($plugintypes); - foreach ($plugintypes as $type => $unused) { - $plugs = get_plugin_list($type); - ksort($plugs); - foreach ($plugs as $plug => $fullplug) { - // Look for tests recursively - if (self::directory_has_tests($fullplug)) { - $pluginswithtests[$type . '_' . $plug] = $fullplug; - } - } - } - return $pluginswithtests; - } - - /** - * Returns all the subsystems having PHPUnit tests - * - * Note we are hacking here the list of subsystems - * to cover some well-known subsystems that are not properly - * returned by the {@link get_core_subsystems()} function. - * - * @return array all the subsystems having PHPUnit tests - */ - private static function get_all_subsystems_with_tests() { - global $CFG; - - $subsystemswithtests = array(); - - $subsystems = get_core_subsystems(); - - // Hack the list a bit to cover some well-known ones - $subsystems['backup'] = 'backup'; - $subsystems['db-dml'] = 'lib/dml'; - $subsystems['db-ddl'] = 'lib/ddl'; - - ksort($subsystems); - foreach ($subsystems as $subsys => $relsubsys) { - if ($relsubsys === null) { - continue; - } - $fullsubsys = $CFG->dirroot . '/' . $relsubsys; - if (!is_dir($fullsubsys)) { - continue; - } - // Look for tests recursively - if (self::directory_has_tests($fullsubsys)) { - $subsystemswithtests['core_' . $subsys] = $fullsubsys; - } - } - return $subsystemswithtests; - } - - /** - * Returns all the directories having tests - * - * @return array all directories having tests - */ - private static function get_all_directories_with_tests() { - global $CFG; - - $dirs = array(); - $dirite = new RecursiveDirectoryIterator($CFG->dirroot); - $iteite = new RecursiveIteratorIterator($dirite); - $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); - $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|'); - foreach ($regite as $path => $element) { - $key = dirname(dirname($path)); - $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_'); - $dirs[$key] = $value; - } - ksort($dirs); - return array_flip($dirs); - } - - /** - * Returns if a given directory has tests (recursively) - * - * @param $dir string full path to the directory to look for phpunit tests - * @return bool if a given directory has tests (true) or no (false) - */ - private static function directory_has_tests($dir) { - if (!is_dir($dir)) { - return false; - } - - $dirite = new RecursiveDirectoryIterator($dir); - $iteite = new RecursiveIteratorIterator($dirite); - $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); - $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|'); - $regite->rewind(); - if ($regite->valid()) { - return true; - } - return false; - } - /** * To be called from debugging() only. * @param string $message diff --git a/lib/phpunit/lib.php b/lib/phpunit/lib.php index 64d72f9e0fd..ac30f623e94 100644 --- a/lib/phpunit/lib.php +++ b/lib/phpunit/lib.php @@ -37,3 +37,4 @@ 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__.'/../testing/classes/test_lock.php'); +require_once(__DIR__.'/../testing/classes/tests_finder.php'); diff --git a/lib/testing/classes/tests_finder.php b/lib/testing/classes/tests_finder.php new file mode 100644 index 00000000000..0a89cfa586c --- /dev/null +++ b/lib/testing/classes/tests_finder.php @@ -0,0 +1,190 @@ +. + +/** + * Tests finder + * + * @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 + */ + +/** + * Finds components and plugins with tests + * + * @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 + */ +class tests_finder { + + /** + * Returns all the components with tests of the specified type + * @param string $testtype The kind of test we are looking for + * @return array + */ + public static function get_components_with_tests($testtype) { + + // Get all the components + $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype); + + // Get all the directories having tests + $directories = self::get_all_directories_with_tests($testtype); + + // Find any directory not covered by proper components + $remaining = array_diff($directories, $components); + + // Add them to the list of components + $components += $remaining; + + return $components; + } + + /** + * Returns all the plugins having tests + * @param string $testtype The kind of test we are looking for + * @return array all the plugins having tests + */ + private static function get_all_plugins_with_tests($testtype) { + $pluginswithtests = array(); + + $plugintypes = get_plugin_types(); + ksort($plugintypes); + foreach ($plugintypes as $type => $unused) { + $plugs = get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug => $fullplug) { + // Look for tests recursively + if (self::directory_has_tests($fullplug, $testtype)) { + $pluginswithtests[$type . '_' . $plug] = $fullplug; + } + } + } + return $pluginswithtests; + } + + /** + * Returns all the subsystems having tests + * + * Note we are hacking here the list of subsystems + * to cover some well-known subsystems that are not properly + * returned by the {@link get_core_subsystems()} function. + * + * @param string $testtype The kind of test we are looking for + * @return array all the subsystems having tests + */ + private static function get_all_subsystems_with_tests($testtype) { + global $CFG; + + $subsystemswithtests = array(); + + $subsystems = get_core_subsystems(); + + // Hack the list a bit to cover some well-known ones + $subsystems['backup'] = 'backup'; + $subsystems['db-dml'] = 'lib/dml'; + $subsystems['db-ddl'] = 'lib/ddl'; + + ksort($subsystems); + foreach ($subsystems as $subsys => $relsubsys) { + if ($relsubsys === null) { + continue; + } + $fullsubsys = $CFG->dirroot . '/' . $relsubsys; + if (!is_dir($fullsubsys)) { + continue; + } + // Look for tests recursively + if (self::directory_has_tests($fullsubsys, $testtype)) { + $subsystemswithtests['core_' . $subsys] = $fullsubsys; + } + } + return $subsystemswithtests; + } + + /** + * Returns all the directories having tests + * + * @param string $testtype The kind of test we are looking for + * @return array all directories having tests + */ + private static function get_all_directories_with_tests($testtype) { + global $CFG; + + $dirs = array(); + $dirite = new RecursiveDirectoryIterator($CFG->dirroot); + $iteite = new RecursiveIteratorIterator($dirite); + $regexp = self::get_regexp($testtype); + $regite = new RegexIterator($iteite, $regexp); + foreach ($regite as $path => $element) { + $key = dirname(dirname($path)); + $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_'); + $dirs[$key] = $value; + } + ksort($dirs); + return array_flip($dirs); + } + + /** + * Returns if a given directory has tests (recursively) + * + * @param string $dir full path to the directory to look for phpunit tests + * @param string $testtype phpunit|behat + * @return bool if a given directory has tests (true) or no (false) + */ + private static function directory_has_tests($dir, $testtype) { + if (!is_dir($dir)) { + return false; + } + + $dirite = new RecursiveDirectoryIterator($dir); + $iteite = new RecursiveIteratorIterator($dirite); + $regexp = self::get_regexp($testtype); + $regite = new RegexIterator($iteite, $regexp); + $regite->rewind(); + if ($regite->valid()) { + return true; + } + return false; + } + + + /** + * Returns the regular expression to match by the test files + * @param string $testtype + * @return string + */ + private static function get_regexp($testtype) { + + $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); + + switch ($testtype) { + case 'phpunit': + $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|'; + break; + case 'features': + $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|'; + break; + case 'stepsdefinitions': + $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.php$|'; + break; + } + + return $regexp; + } +} From 0ea35584ae29046926edf5b57044e5120f9d0f03 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Fri, 11 Jan 2013 17:34:24 +0800 Subject: [PATCH 3/3] MDL-37458 testing common methods generalization --- admin/tool/phpunit/cli/init.php | 1 + admin/tool/phpunit/cli/util.php | 3 +- lib/phpunit/bootstrap.php | 3 +- lib/phpunit/bootstraplib.php | 95 +-- lib/phpunit/classes/hint_resultprinter.php | 2 +- lib/phpunit/classes/util.php | 637 +----------------- lib/phpunit/lib.php | 2 +- lib/testing/classes/test_lock.php | 2 +- lib/testing/classes/util.php | 727 +++++++++++++++++++++ lib/testing/lib.php | 130 ++++ 10 files changed, 892 insertions(+), 710 deletions(-) create mode 100644 lib/testing/classes/util.php create mode 100644 lib/testing/lib.php diff --git a/admin/tool/phpunit/cli/init.php b/admin/tool/phpunit/cli/init.php index ca00d564934..bf026a439c2 100644 --- a/admin/tool/phpunit/cli/init.php +++ b/admin/tool/phpunit/cli/init.php @@ -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"; diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php index a7552fe8e43..111be5d26d1 100644 --- a/admin/tool/phpunit/cli/util.php +++ b/admin/tool/phpunit/cli/util.php @@ -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); diff --git a/lib/phpunit/bootstrap.php b/lib/phpunit/bootstrap.php index 6ead09fec42..91ea8e6e19e 100644 --- a/lib/phpunit/bootstrap.php +++ b/lib/phpunit/bootstrap.php @@ -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 diff --git a/lib/phpunit/bootstraplib.php b/lib/phpunit/bootstraplib.php index b7c9fde28a6..28f4345a829 100644 --- a/lib/phpunit/bootstraplib.php +++ b/lib/phpunit/bootstraplib.php @@ -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); } diff --git a/lib/phpunit/classes/hint_resultprinter.php b/lib/phpunit/classes/hint_resultprinter.php index 4f68206702b..a04b32628d2 100644 --- a/lib/phpunit/classes/hint_resultprinter.php +++ b/lib/phpunit/classes/hint_resultprinter.php @@ -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'; } diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index f08f490cf64..3dcf545c6e9 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -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. @@ -32,28 +33,10 @@ * @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 version hash in the database and in a file. + self::store_versions_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)); - - 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 { ''.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1', $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 diff --git a/lib/phpunit/lib.php b/lib/phpunit/lib.php index ac30f623e94..2ceaf3e9860 100644 --- a/lib/phpunit/lib.php +++ b/lib/phpunit/lib.php @@ -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'); diff --git a/lib/testing/classes/test_lock.php b/lib/testing/classes/test_lock.php index b48bfd0e163..710cdeabcf4 100644 --- a/lib/testing/classes/test_lock.php +++ b/lib/testing/classes/test_lock.php @@ -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 index 00000000000..8588b1baf3c --- /dev/null +++ b/lib/testing/classes/util.php @@ -0,0 +1,727 @@ +. + +/** + * 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 index 00000000000..0713d6bde98 --- /dev/null +++ b/lib/testing/lib.php @@ -0,0 +1,130 @@ +. + +/** + * 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); +}