diff --git a/.gitignore b/.gitignore index 5a123491042..a57c23f7201 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ CVS /.project /.buildpath /.cache +/phpunit.xml \ No newline at end of file diff --git a/admin/tool/phpunit/cli/util.php b/admin/tool/phpunit/cli/util.php new file mode 100644 index 00000000000..c270e172633 --- /dev/null +++ b/admin/tool/phpunit/cli/util.php @@ -0,0 +1,93 @@ +. + +/** + * PHPUnit related utilities. + * + * Exit codes: + * 0 - success + * 1 - general error + * 130 - coding error + * 131 - configuration problem + * 133 - drop existing data before installing + * + * @package tool_phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('PHPUNIT_CLI_UTIL', true); + +require(__DIR__ . '/../../../../lib/phpunit/bootstrap.php'); +require_once($CFG->libdir.'/phpunit/lib.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/upgradelib.php'); +require_once($CFG->libdir.'/clilib.php'); +require_once($CFG->libdir.'/pluginlib.php'); +require_once($CFG->libdir.'/installlib.php'); + +// now get cli options +list($options, $unrecognized) = cli_get_params( + array( + 'drop' => false, + 'install' => false, + 'buildconfig' => false, + 'help' => false, + ), + array( + 'h' => 'help' + ) +); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +$drop = $options['drop']; +$install = $options['install']; +$buildconfig = $options['buildconfig']; + +if ($options['help'] or (!$drop and !$install and !$buildconfig)) { + $help = "Various PHPUnit utility functions + +Options: +--drop Drop database and dataroot +--install Install database +--buildconfig Build /phpunit.xml from /phpunit.xml.dist that includes suites for all plugins and core + +-h, --help Print out this help + +Example: +\$/usr/bin/php lib/phpunit/tool.php +"; + echo $help; + die; +} + +if ($buildconfig) { + phpunit_util::build_config_file(); + exit(0); + +} else if ($drop) { + phpunit_util::drop_site(); + // note: we must stop here because $CFG is messed up and we can not reinstall, sorry + exit(0); + +} else if ($install) { + phpunit_util::install_site(); + exit(0); +} diff --git a/admin/tool/phpunit/index.php b/admin/tool/phpunit/index.php new file mode 100644 index 00000000000..5c55e7091db --- /dev/null +++ b/admin/tool/phpunit/index.php @@ -0,0 +1,39 @@ +. + +/** + * PHPUnit info + * + * @package tool_phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + */ + +define('NO_OUTPUT_BUFFERING', true); + +require(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +admin_externalpage_setup('toolphpunit'); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('pluginname', 'tool_phpunit')); +echo $OUTPUT->box_start(); + +$info = file_get_contents("$CFG->libdir/phpunit/readme.md"); +echo markdown_to_html($info); + +echo $OUTPUT->box_end(); +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/admin/tool/phpunit/lang/en/tool_phpunit.php b/admin/tool/phpunit/lang/en/tool_phpunit.php new file mode 100644 index 00000000000..08dbbbdb395 --- /dev/null +++ b/admin/tool/phpunit/lang/en/tool_phpunit.php @@ -0,0 +1,25 @@ +. + +/** + * Strings for component 'tool_phpunit' + * + * @package tool_phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'PHPUnit tests'; diff --git a/admin/tool/phpunit/settings.php b/admin/tool/phpunit/settings.php new file mode 100644 index 00000000000..8e06b2f4b78 --- /dev/null +++ b/admin/tool/phpunit/settings.php @@ -0,0 +1,28 @@ +. + +/** + * PHPunit integration + * + * @package tool_phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die; + +$ADMIN->add('development', new admin_externalpage('toolphpunit', get_string('pluginname', 'tool_phpunit'), "$CFG->wwwroot/$CFG->admin/tool/phpunit/index.php")); diff --git a/admin/tool/phpunit/version.php b/admin/tool/phpunit/version.php new file mode 100644 index 00000000000..e745026f858 --- /dev/null +++ b/admin/tool/phpunit/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package tool_phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2012030800; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2012030100; // Requires this Moodle version +$plugin->component = 'tool_phpunit'; // Full name of the plugin (used for diagnostics) + diff --git a/config-dist.php b/config-dist.php index abc8da1b26d..e4fe58fba35 100644 --- a/config-dist.php +++ b/config-dist.php @@ -479,7 +479,13 @@ $CFG->admin = 'admin'; // Example: // $CFG->forced_plugin_settings = array('pluginname' => array('settingname' => 'value', 'secondsetting' => 'othervalue'), // 'otherplugin' => array('mysetting' => 'myvalue', 'thesetting' => 'thevalue')); - +// +//========================================================================= +// 9. PHPUNIT SUPPORT +//========================================================================= +// $CFG->phpunit_prefix = 'phpu_'; +// $CFG->phpunit_dataroot = '/home/example/phpu_moodledata'; +// $CFG->phpunit_directorypermissions = 02777; // optional //========================================================================= // ALL DONE! To continue installation, visit your main page with a browser diff --git a/lib/accesslib.php b/lib/accesslib.php index bd45588f988..62e7337ad65 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -217,7 +217,7 @@ $ACCESSLIB_PRIVATE->capabilities = null; // detailed information about th */ function accesslib_clear_all_caches_for_unit_testing() { global $UNITTEST, $USER; - if (empty($UNITTEST->running)) { + if (empty($UNITTEST->running) and !PHPUNITTEST) { throw new coding_exception('You must not call clear_all_caches outside of unit tests.'); } diff --git a/lib/ddl/database_manager.php b/lib/ddl/database_manager.php index 22d24e43b13..c4409d02d01 100644 --- a/lib/ddl/database_manager.php +++ b/lib/ddl/database_manager.php @@ -112,18 +112,15 @@ class database_manager { /** * Reset a sequence to the id field of a table. - * @param string $table Name of table. - * @throws ddl_exception|ddl_table_missing_exception Exception thrown upon reset errors. + * @param string|xmldb_table $table Name of table. + * @throws ddl_exception thrown upon reset errors. */ public function reset_sequence($table) { if (!is_string($table) and !($table instanceof xmldb_table)) { throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!'); } - /// Check the table exists - if (!$this->table_exists($table)) { - throw new ddl_table_missing_exception($table); - } + // Do not test if table exists because it is slow if (!$sqlarr = $this->generator->getResetSequenceSQL($table)) { throw new ddl_exception('ddlunknownerror', null, 'table reset sequence sql not generated'); diff --git a/lib/ddl/simpletest/testddl.php b/lib/ddl/simpletest/testddl.php index 4a27f80cf94..5d452a0396e 100644 --- a/lib/ddl/simpletest/testddl.php +++ b/lib/ddl/simpletest/testddl.php @@ -8,6 +8,8 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } +global $CFG; + require_once($CFG->libdir . '/adminlib.php'); class ddl_test extends UnitTestCase { diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 0099f60bfc9..fd2765d254b 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -7763,7 +7763,7 @@ function get_plugin_types($fullpaths=true) { function get_plugin_list($plugintype) { global $CFG; - $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'phpunit'); + $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests'); if ($plugintype == 'auth') { // Historically we have had an auth plugin called 'db', so allow a special case. $key = array_search('db', $ignored); diff --git a/lib/phpunit/bootstrap.php b/lib/phpunit/bootstrap.php new file mode 100644 index 00000000000..0dd6106423d --- /dev/null +++ b/lib/phpunit/bootstrap.php @@ -0,0 +1,208 @@ +. + +/** + * Prepares PHPUnit environment, it is called automatically. + * + * Exit codes: + * 0 - success + * 1 - general error + * 130 - coding error + * 131 - configuration problem + * 132 - drop data, then install new test database + * + * @package core_core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// we want to know about all problems +error_reporting(E_ALL | E_STRICT); +ini_set('display_errors', '1'); +ini_set('log_errors', '1'); + +if (isset($_SERVER['REMOTE_ADDR'])) { + phpunit_bootstrap_error('Unit tests can be executed only from commandline!', 1); +} + +if (defined('PHPUNITTEST')) { + phpunit_bootstrap_error("PHPUNITTEST constant must not be manually defined anywhere!", 130); +} +define('PHPUNITTEST', true); + +if (defined('CLI_SCRIPT')) { + phpunit_bootstrap_error('CLI_SCRIPT must not be manually defined in any PHPUnit test scripts', 130); +} +define('CLI_SCRIPT', true); + +define('NO_OUTPUT_BUFFERING', true); + +// only load CFG from config.php +define('ABORT_AFTER_CONFIG', true); +require(__DIR__ . '/../../config.php'); + +// remove error handling overrides done in config.php +error_reporting(E_ALL); +ini_set('display_errors', '1'); +ini_set('log_errors', '1'); + +// prepare dataroot +umask(0); +if (isset($CFG->phpunit_directorypermissions)) { + $CFG->directorypermissions = $CFG->phpunit_directorypermissions; +} else { + $CFG->directorypermissions = 02777; +} +$CFG->filepermissions = ($CFG->directorypermissions & 0666); +if (!isset($CFG->phpunit_dataroot)) { + phpunit_bootstrap_error('Missing $CFG->phpunit_dataroot in config.php, can not run tests!', 131); +} +if (isset($CFG->dataroot) and $CFG->phpunit_dataroot === $CFG->dataroot) { + phpunit_bootstrap_error('$CFG->dataroot and $CFG->phpunit_dataroot must not be identical, can not run tests!', 131); +} +if (!file_exists($CFG->phpunit_dataroot)) { + mkdir($CFG->phpunit_dataroot, $CFG->directorypermissions); +} +if (!is_dir($CFG->phpunit_dataroot)) { + phpunit_bootstrap_error('$CFG->phpunit_dataroot directory can not be created, can not run tests!', 131); +} +if (!is_writable($CFG->phpunit_dataroot)) { + // try to fix premissions if possible + if (function_exists('posix_getuid')) { + $chmod = fileperms($CFG->phpunit_dataroot); + if (fileowner($dir) == posix_getuid()) { + $chmod = $chmod | 0700; + chmod($CFG->phpunit_dataroot, $chmod); + } + } + if (!is_writable($CFG->phpunit_dataroot)) { + phpunit_bootstrap_error('$CFG->phpunit_dataroot directory is not writable, can not run tests!', 131); + } +} +if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) { + if ($dh = opendir($CFG->phpunit_dataroot)) { + while (($file = readdir($dh)) !== false) { + if ($file === 'phpunit' or $file === '.' or $file === '..' or $file === '.DS_store') { + continue; + } + phpunit_bootstrap_error('$CFG->phpunit_dataroot directory is not empty, can not run tests! Is it used for anything else?', 131); + } + closedir($dh); + unset($dh); + unset($file); + } + + // now we are 100% sure this dir is used only for phpunit tests + phpunit_bootstrap_initdataroot($CFG->phpunit_dataroot); +} + + +// verify db prefix +if (!isset($CFG->phpunit_prefix)) { + phpunit_bootstrap_error('Missing $CFG->phpunit_dataroot in config.php, can not run tests!', 131); +} +if (isset($CFG->prefix) and $CFG->prefix === $CFG->phpunit_prefix) { + phpunit_bootstrap_error('$CFG->prefix and $CFG->phpunit_prefix must not be identical, can not run tests!', 131); +} + +// throw away standard CFG settings + +$CFG->dataroot = $CFG->phpunit_dataroot; +$CFG->prefix = $CFG->phpunit_prefix; + +$allowed = array('wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions', + 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions'); +$productioncfg = (array)$CFG; +$CFG = new stdClass(); +foreach ($productioncfg as $key=>$value) { + if (!in_array($key, $allowed) and strpos($key, 'phpunit_') !== 0) { + // ignore + continue; + } + $CFG->{$key} = $value; +} +unset($key); +unset($value); +unset($allowed); +unset($productioncfg); + +// force the same CFG settings in all sites +$CFG->debug = (E_ALL | E_STRICT | 38911); // can not use DEBUG_DEVELOPER here +$CFG->debugdisplay = 1; +error_reporting($CFG->debug); +ini_set('display_errors', '1'); +ini_set('log_errors', '0'); + +$CFG->noemailever = true; // better not mail anybody from tests, override temporarily if necessary +$CFG->cachetext = 0; // disable this very nasty setting + +// some ugly hacks +$CFG->themerev = 1; +$CFG->jsrev = 1; + +// load test case stub classes and other stuff +require_once("$CFG->dirroot/lib/phpunit/lib.php"); + +// finish moodle init +define('ABORT_AFTER_CONFIG_CANCEL', true); +require("$CFG->dirroot/lib/setup.php"); + +raise_memory_limit(MEMORY_EXTRA); + +if (defined('PHPUNIT_CLI_UTIL')) { + // all other tests are done in the CLI scripts... + return; +} + +if (!phpunit_util::is_testing_ready()) { + phpunit_bootstrap_error('Database is not initialised to run unit tests, please use "php admin/tool/phpunit/cli/util.php --install"', 132); +} + +// refresh data in all tables, clear caches, etc. +phpunit_util::reset_all_data(); + +// store fresh globals +phpunit_util::init_globals(); + + +//========================================================= + +/** + * Print error and stop execution + * @param $text + * @param int $errorcode + * @return void - stops code execution with error code + */ +function phpunit_bootstrap_error($text, $errorcode = 1) { + fwrite(STDERR, $text."\n"); + exit($errorcode); +} + +/** + * Mark empty dataroot to be used for testing. + * @param $dataroot + * @return void + */ +function phpunit_bootstrap_initdataroot($dataroot) { + global $CFG; + + file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!'); + chmod("$dataroot/phpunittestdir.txt", $CFG->filepermissions); + if (!file_exists("$CFG->phpunit_dataroot/phpunit")) { + mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions); + } +} diff --git a/lib/phpunit/lib.php b/lib/phpunit/lib.php new file mode 100644 index 00000000000..41f7974e94d --- /dev/null +++ b/lib/phpunit/lib.php @@ -0,0 +1,602 @@ +. + +/** + * Various PHPUnit classes and functions + * + * @package core_core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once 'PHPUnit/Autoload.php'; // necessary when loaded from cli/util.php script + + +/** + * Collection of utility methods. + * + * @package core_phpunit + * @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 array original content of all database tables + */ + protected static $tabledata = null; + + protected static $globals = array(); + + /** + * Returns contents of all tables right after installation. + * @static + * @return array $table=>$records + */ + protected static function get_tabledata() { + global $CFG; + + 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('Can not read dataroot/phpunit/tabledata.ser or invalid format!'); + } + + return self::$tabledata; + } + + /** + * Initialise CFG using data from fresh new install. + * @static + */ + public static function initialise_cfg() { + global $CFG, $DB; + + if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) { + // most probably PHPUnit CLI installer + return; + } + + if (!$DB->get_manager()->table_exists('config') or !$DB->count_records('config')) { + @unlink("$CFG->dataroot/phpunit/tabledata.ser"); + @unlink("$CFG->dataroot/phpunit/versionshash.txt"); + self::$tabledata = null; + return; + } + + $data = self::get_tabledata(); + + foreach($data['config'] as $record) { + $name = $record->name; + $value = $record->value; + if (property_exists($CFG, $name)) { + // config.php settings always take precedence + continue; + } + $CFG->{$name} = $value; + } + } + + /** + * Reset contents of all database tables to initial values, reset caches, etc. + * + * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! + * + * @static + */ + public static function reset_all_data() { + global $DB, $CFG; + + $data = self::get_tabledata(); + + $trans = $DB->start_delegated_transaction(); // faster and safer + foreach ($data as $table=>$records) { + $DB->delete_records($table, array()); + $resetseq = null; + foreach ($records as $record) { + if (is_null($resetseq)) { + $resetseq = property_exists($record, 'id'); + } + $DB->import_record($table, $record, false, true); + } + if ($resetseq === true) { + $DB->get_manager()->reset_sequence($table, true); + } + } + $trans->allow_commit(); + + purge_all_caches(); + + $user = new stdClass(); + $user->id = 0; + $user->mnet = 0; + $user->mnethostid = $CFG->mnet_localhost_id; + session_set_user($user); + accesslib_clear_all_caches_for_unit_testing(); + } + + /** + * Called during bootstrap only! + * @static + * @return void + */ + public static function init_globals() { + global $CFG; + + self::$globals['CFG'] = clone($CFG); + } + + /** + * Returns original state of global variable. + * @static + * @param string $name + * @return mixed + */ + public static function get_global_backup($name) { + if (isset(self::$globals[$name])) { + if (is_object(self::$globals[$name])) { + $return = clone(self::$globals[$name]); + return $return; + } else { + return self::$globals[$name]; + } + } + 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 anway 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? + * + * @static + * @return bool + */ + public static function is_testing_ready() { + global $DB, $CFG; + + if (!self::is_test_site()) { + return false; + } + + $tables = $DB->get_tables(true); + + if (!$tables) { + return false; + } + + if (!get_config('core', 'phpunittest')) { + return false; + } + + if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) { + return false; + } + + if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) { + return false; + } + + $hash = phpunit_util::get_version_hash(); + $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt"); + + if ($hash !== $oldhash) { + return false; + } + + return true; + } + + /** + * Drop all test site data. + * + * Note: To be used from CLI scripts only. + * + * @static + * @return void, may terminate execution with exit code + */ + public static function drop_site() { + global $DB, $CFG; + + if (!self::is_test_site()) { + cli_error('Can not drop non-test sites!!', 131); + } + + // drop dataroot + remove_dir($CFG->dataroot, true); + phpunit_bootstrap_initdataroot($CFG->dataroot); + + // drop all tables + $trans = $DB->start_delegated_transaction(); + $tables = $DB->get_tables(false); + foreach ($tables as $tablename) { + $DB->delete_records($tablename, array()); + } + $trans->allow_commit(); + + // now drop them + foreach ($tables as $tablename) { + $table = new xmldb_table($tablename); + $DB->get_manager()->drop_table($table); + } + } + + /** + * Perform a fresh test site installation + * + * Note: To be used from CLI scripts only. + * + * @static + * @return void, may terminate execution with exit code + */ + public static function install_site() { + global $DB, $CFG; + + if (!self::is_test_site()) { + cli_error('Can not install non-test sites!!', 131); + } + + if ($DB->get_tables()) { + cli_error('Database tables already installed, drop the site first.', 133); + } + + $options = array(); + $options['adminpass'] = 'admin'; // removed later + $options['shortname'] = 'phpunit'; + $options['fullname'] = 'PHPUnit test site'; + + install_cli_database($options, false); + + // just in case remove admin password so that normal login is not possible + $DB->set_field('user', 'password', 'not cached', array('username' => 'admin')); + + // add test db flag + set_config('phpunittest', 'phpunittest'); + + // store data for all tables + $data = array(); + $tables = $DB->get_tables(); + foreach ($tables as $table) { + $data[$table] = $DB->get_records($table, array()); + } + $data = serialize($data); + @unlink("$CFG->dataroot/phpunit/tabledata.ser"); + file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data); + + // hash all plugin versions - helps with very fast detection of db structure changes + $hash = phpunit_util::get_version_hash(); + @unlink("$CFG->dataroot/phpunit/versionshash.txt"); + file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash); + } + + /** + * Culculate unique version hash for all available plugins and core. + * @static + * @return string sha1 hash + */ + public static function get_version_hash() { + global $CFG; + + $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; + } + } + + $hash = sha1(serialize($versions)); + + return $hash; + } + + /** + * Builds /phpunit.xml file using defaults from /phpunit.xml.dist + * @static + * @return void + */ + public static function build_config_file() { + global $CFG; + + $template = ' + + + @dir@ + + '; + $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); + + $suites = ''; + + $plugintypes = get_plugin_types(); + ksort($plugintypes); + foreach ($plugintypes as $type=>$unused) { + $plugs = get_plugin_list($type); + ksort($plugs); + foreach ($plugs as $plug=>$fullplug) { + if (!file_exists("$fullplug/tests/")) { + continue; + } + $dir = preg_replace("|$CFG->dirroot/|", '', $fullplug, 1); + $dir .= '/tests'; + $component = $type.'_'.$plug; + + $suite = str_replace('@component@', $component, $template); + $suite = str_replace('@dir@', $dir, $suite); + + $suites .= $suite; + } + } + + $data = preg_replace('|.*|s', $suites, $data, 1); + + @unlink("$CFG->dirroot/phpunit.xml"); + file_put_contents("$CFG->dirroot/phpunit.xml", $data); + } +} + + +/** + * Simplified emulation test case for legacy SimpleTest. + * + * Note: this is supposed to work for very simple tests only. + * + * @deprecated + * @package core_phpunit + * @author Petr Skoda + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class UnitTestCase extends PHPUnit_Framework_TestCase { + + /** + * @deprecated + * @param bool $expected + * @param string $message + * @return void + */ + public function expectException($expected, $message = '') { + // use phpdocs: @expectedException ExceptionClassName + if (!$expected) { + return; + } + $this->setExpectedException('moodle_exception', $message); + } + + /** + * @deprecated + * @param bool $expected + * @param string $message + * @return void + */ + public static function expectError($expected = false, $message = '') { + // not available in PHPUnit + if (!$expected) { + return; + } + self::skipIf(true); + } + + /** + * @deprecated + * @static + * @param mixed $actual + * @param string $messages + * @return void + */ + public static function assertTrue($actual, $messages = '') { + parent::assertTrue((bool)$actual, $messages); + } + + /** + * @deprecated + * @static + * @param mixed $actual + * @param string $messages + * @return void + */ + public static function assertFalse($actual, $messages = '') { + parent::assertFalse((bool)$actual, $messages); + } + + /** + * @deprecated + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertEqual($expected, $actual, $message = '') { + parent::assertEquals($expected, $actual, $message); + } + + /** + * @deprecated + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertNotEqual($expected, $actual, $message = '') { + parent::assertNotEquals($expected, $actual, $message); + } + + /** + * @deprecated + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertIdentical($expected, $actual, $message = '') { + parent::assertSame($expected, $actual, $message); + } + + /** + * @deprecated + * @static + * @param mixed $expected + * @param mixed $actual + * @param string $message + * @return void + */ + public static function assertNotIdentical($expected, $actual, $message = '') { + parent::assertNotSame($expected, $actual, $message); + } + + /** + * @deprecated + * @static + * @param mixed $actual + * @param mixed $expected + * @param string $message + * @return void + */ + public static function assertIsA($actual, $expected, $message = '') { + parent::assertInstanceOf($expected, $actual, $message); + } +} + + +/** + * The simplest PHPUnit test case customised for Moodle, + * do not modify database or any globals. + * + * @package core_phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class basic_testcase extends PHPUnit_Framework_TestCase { + + /** + * Constructs a test case with the given name. + * + * @param string $name + * @param array $data + * @param string $dataName + */ + public function __construct($name = NULL, array $data = array(), $dataName = '') { + parent::__construct($name, $data, $dataName); + + $this->setBackupGlobals(false); + $this->setBackupStaticAttributes(false); + $this->setRunTestInSeparateProcess(false); + $this->setInIsolation(false); + } + + /** + * Runs the bare test sequence. + * @return void + */ + public function runBare() { + global $CFG, $USER, $DB; + + $dbwrites = $DB->perf_get_writes(); + + parent::runBare(); + + $oldcfg = phpunit_util::get_global_backup('CFG'); + foreach($CFG as $k=>$v) { + if (!property_exists($oldcfg, $k)) { + unset($CFG->$k); + error_log('warning: unexpected new $CFG->'.$k.' value in testcase: '.get_class($this).'->'.$this->getName(true)); + } else if ($oldcfg->$k !== $CFG->$k) { + $CFG->$k = $oldcfg->$k; + error_log('warning: unexpected change of $CFG->'.$k.' value in testcase: '.get_class($this).'->'.$this->getName(true)); + } + unset($oldcfg->$k); + + } + if ($oldcfg) { + foreach($oldcfg as $k=>$v) { + $CFG->$k = $v; + error_log('warning: unexpected removal of $CFG->'.$k.' in testcase: '.get_class($this).'->'.$this->getName(true)); + } + } + + if ($USER->id != 0) { + error_log('warning: unexpected change of $USER in testcase: '.get_class($this).'->'.$this->getName(true)); + $USER = new stdClass(); + $USER->id = 0; + } + + if ($dbwrites != $DB->perf_get_writes()) { + //TODO: find out what was changed exactly + error_log('warning: unexpected database modification, resetting DB state in testcase: '.get_class($this).'->'.$this->getName(true)); + phpunit_util::reset_all_data(); + } + + //TODO: somehow find out if there are changes in dataroot + } +} + diff --git a/lib/phpunit/readme.md b/lib/phpunit/readme.md new file mode 100644 index 00000000000..5a67f11f05a --- /dev/null +++ b/lib/phpunit/readme.md @@ -0,0 +1,49 @@ +PHPUnit testing support in Moodle +================================== + + +Installation +------------ +1. install PHPUnit PEAR extension - see [PHPUnit docs](http://www.phpunit.de/manual/current/en/installation.html) for more details +2. edit main config.php - add $CFG->phpunit_prefix and $CFG->phpunit_dataroot - see config-dist.php for more details +3. execute `php admin/tool/phpunit/cli/util.php --install` to initialise test database +4. it is necessary to reinitialise the test database manually after every upgrade or installation of new plugins + + +Test execution +-------------- +* optionally generate phpunit.xml by executing `php admin/tool/phpunit/cli/util.php --buildconfig` - it collects test cases from all plugins +* execute `phpunit` shell command from dirroot directory +* you can also execute a single test `phpunit core_phpunit_basic_testcase lib/tests/phpunit_test.php` +* or all tests in one directory `phpunit --configuration phpunit.xml lib/tests/*_test.php` +* it is possible to create custom configuration files in xml format and use `phpunit -c myconfig.xml` + + +How to add more tests +--------------------- +1. create `tests` directory in any plugin +2. add `*_test.php` files with custom class that extends `basic_testcase` +3. manually add all core unit test locations to `phpunit.xml.dist` + + +How to convert existing tests +----------------------------- +1. create new test file in `xxx/tests/yyy_test.php` +2. copy contents of the old test file +3. replace `extends UnitTestCase` with `extends basic_testcase` +4. fix setUp, tearDown, asserts, etc. +5. some old SimpleTest tests can be executed directly - mocking, database operations, assert(), etc. does not work, you may need to add `global $CFG;` before includes + + +FAQs +---- +* Why is it necessary to execute the tests from commandline? PHPUnit is designed to be executed from shell, existing Moodle globals and constants would interfere with it. +* Why `tests` subdirectory? It should not collide with any plugin name because plugin names use singular form. +* Why is it necessary to include core and plugin suites in configuration files? PHPUnit does not seem to allow dynamic loading of tests from our dir structure. + + +TODO +---- +* stage 2 - implement advaced_testcase - support for database modifications, object generators, automatic rollback of db, blobals and dataroot +* stage 3 - mocking and other advanced features, add support for execution of functional DB tests for different engines together (new options in phpunit.xml) +* other - support for execution of tests and cli/util.php from web UI (to be implemented via shell execution), shell script that prepares everything for the first execution diff --git a/lib/setup.php b/lib/setup.php index 2f0f1e4f1fa..d5febeb2c61 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -45,7 +45,7 @@ global $CFG; // this should be done much earlier in config.php before creating new $CFG instance if (!isset($CFG)) { - if (defined('PHPUNIT_SCRIPT') and PHPUNIT_SCRIPT) { + if (defined('PHPUNITTEST') and PHPUNITTEST) { echo('There is a missing "global $CFG;" at the beginning of the config.php file.'."\n"); exit(1); } else { @@ -112,7 +112,10 @@ if (!isset($CFG->cachedir)) { // directory of the script when run from the command line. The require_once() // would fail, so we'll have to chdir() if (!isset($_SERVER['REMOTE_ADDR']) && isset($_SERVER['argv'][0])) { - chdir(dirname($_SERVER['argv'][0])); + // do it only once - skip the second time when continuing after prevous abort + if (!defined('ABORT_AFTER_CONFIG') and !defined('ABORT_AFTER_CONFIG_CANCEL')) { + chdir(dirname($_SERVER['argv'][0])); + } } // sometimes default PHP settings are borked on shared hosting servers, I wonder why they have to do that?? @@ -130,6 +133,11 @@ if (!defined('NO_OUTPUT_BUFFERING')) { define('NO_OUTPUT_BUFFERING', false); } +// PHPUnit tests need custom init +if (!defined('PHPUNITTEST')) { + define('PHPUNITTEST', false); +} + // Servers should define a default timezone in php.ini, but if they don't then make sure something is defined. // This is a quick hack. Ideally we should ask the admin for a value. See MDL-22625 for more on this. if (function_exists('date_default_timezone_set') and function_exists('date_default_timezone_get')) { @@ -139,15 +147,6 @@ if (function_exists('date_default_timezone_set') and function_exists('date_defau unset($olddebug); } -// PHPUnit scripts are a special case, for now we treat them as normal CLI scripts, -// please note you must install PHPUnit library separately via PEAR -if (!defined('PHPUNIT_SCRIPT')) { - define('PHPUNIT_SCRIPT', false); -} -if (PHPUNIT_SCRIPT) { - define('CLI_SCRIPT', true); -} - // Detect CLI scripts - CLI scripts are executed from command line, do not have session and we do not want HTML in output // In your new CLI scripts just add "define('CLI_SCRIPT', true);" before requiring config.php. // Please note that one script can not be accessed from both CLI and web interface. @@ -471,7 +470,11 @@ if (isset($CFG->debug)) { } // Load up any configuration from the config table -initialise_cfg(); +if (PHPUNITTEST) { + phpunit_util::initialise_cfg(); +} else { + initialise_cfg(); +} // Verify upgrade is not running unless we are in a script that needs to execute in any case if (!defined('NO_UPGRADE_CHECK') and isset($CFG->upgraderunning)) { @@ -820,7 +823,7 @@ if (!empty($CFG->customscripts)) { } } -if (CLI_SCRIPT and !defined('WEB_CRON_EMULATED_CLI') and !PHPUNIT_SCRIPT) { +if ((CLI_SCRIPT and !defined('WEB_CRON_EMULATED_CLI')) or PHPUNITTEST) { // no ip blocking } else if (!empty($CFG->allowbeforeblock)) { // allowed list processed before blocked list? // in this case, ip in allowed list will be performed first diff --git a/lib/simpletest/testcomponentlib.php b/lib/simpletest/testcomponentlib.php index 13d15438efa..44a0fe2ced0 100644 --- a/lib/simpletest/testcomponentlib.php +++ b/lib/simpletest/testcomponentlib.php @@ -26,6 +26,9 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + +global $CFG; + require_once($CFG->libdir.'/componentlib.class.php'); class componentlib_test extends UnitTestCase { diff --git a/lib/simpletest/testcsslib.php b/lib/simpletest/testcsslib.php index b69a8a5a10b..abe3e1b3552 100644 --- a/lib/simpletest/testcsslib.php +++ b/lib/simpletest/testcsslib.php @@ -26,6 +26,9 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + +global $CFG; + require_once($CFG->libdir . '/csslib.php'); diff --git a/lib/simpletest/testexternallib.php b/lib/simpletest/testexternallib.php index 709d226f52b..7e1f14a3879 100644 --- a/lib/simpletest/testexternallib.php +++ b/lib/simpletest/testexternallib.php @@ -27,6 +27,9 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + +global $CFG; + require_once($CFG->libdir . '/externallib.php'); class externallib_test extends UnitTestCase { diff --git a/lib/simpletest/testfilelib.php b/lib/simpletest/testfilelib.php index ffea131f2bc..0e63fc8a439 100644 --- a/lib/simpletest/testfilelib.php +++ b/lib/simpletest/testfilelib.php @@ -27,6 +27,9 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + +global $CFG; + require_once($CFG->libdir . '/filelib.php'); class filelib_test extends UnitTestCase { diff --git a/lib/simpletest/testformslib.php b/lib/simpletest/testformslib.php index 2d8d788e2cf..7b41c302eb0 100644 --- a/lib/simpletest/testformslib.php +++ b/lib/simpletest/testformslib.php @@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + require_once($CFG->libdir . '/formslib.php'); require_once($CFG->libdir . '/form/radio.php'); require_once($CFG->libdir . '/form/select.php'); diff --git a/lib/simpletest/testhtmlwriter.php b/lib/simpletest/testhtmlwriter.php index ed3a1e0b7d7..b22c92c2307 100644 --- a/lib/simpletest/testhtmlwriter.php +++ b/lib/simpletest/testhtmlwriter.php @@ -27,6 +27,9 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + +global $CFG; + require_once($CFG->libdir . '/outputcomponents.php'); diff --git a/lib/simpletest/testnavigationlib.php b/lib/simpletest/testnavigationlib.php index fdb5f4f4d09..be5493465ce 100644 --- a/lib/simpletest/testnavigationlib.php +++ b/lib/simpletest/testnavigationlib.php @@ -26,6 +26,9 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } + +global $CFG; + require_once($CFG->libdir . '/navigationlib.php'); class navigation_node_test extends UnitTestCase { @@ -52,7 +55,7 @@ class navigation_node_test extends UnitTestCase { $this->activeurl = $PAGE->url; navigation_node::override_active_url($this->activeurl); - + $this->inactiveurl = new moodle_url('http://www.moodle.com/'); $this->fakeproperties['action'] = $this->inactiveurl; @@ -165,11 +168,11 @@ class navigation_node_test extends UnitTestCase { public function test_find_active_node() { $activenode1 = $this->node->find_active_node(); $activenode2 = $this->node->get('demo1')->find_active_node(); - + if ($this->assertIsA($activenode1, 'navigation_node')) { $this->assertReference($activenode1, $this->node->get('demo3')->get('demo5')->get('activity1')); } - + $this->assertNotA($activenode2, 'navigation_node'); } diff --git a/lib/simpletest/testoutputcomponents.php b/lib/simpletest/testoutputcomponents.php index f8aa788b2ef..6ad6ccc4e20 100644 --- a/lib/simpletest/testoutputcomponents.php +++ b/lib/simpletest/testoutputcomponents.php @@ -24,6 +24,9 @@ */ defined('MOODLE_INTERNAL') || die(); + +global $CFG; + require_once($CFG->libdir . '/outputcomponents.php'); /** @@ -142,7 +145,7 @@ EOF; $this->assertTrue($menu instanceof custom_menu); $this->assertTrue($menu->has_children()); $firstlevel = $menu->get_children(); - $this->assertIsA($firstlevel, 'array'); + $this->assertTrue(is_array($firstlevel)); $this->assertEqual(2, count($firstlevel)); $item = array_shift($firstlevel); diff --git a/lib/simpletest/testquestionlib.php b/lib/simpletest/testquestionlib.php index 2e1f205beb9..6ebacdbd8ea 100644 --- a/lib/simpletest/testquestionlib.php +++ b/lib/simpletest/testquestionlib.php @@ -26,6 +26,8 @@ defined('MOODLE_INTERNAL') || die(); +global $CFG; + require_once($CFG->libdir . '/questionlib.php'); diff --git a/lib/simpletest/testrss.php b/lib/simpletest/testrss.php index add6b099535..d154c8db392 100644 --- a/lib/simpletest/testrss.php +++ b/lib/simpletest/testrss.php @@ -14,6 +14,8 @@ if (!defined('MOODLE_INTERNAL')) { * If networking/proxy configuration is wrong these tests will fail.. */ +global $CFG; + require_once($CFG->libdir.'/simplepie/moodle_simplepie.php'); class moodlesimplepie_test extends UnitTestCase { diff --git a/lib/tests/htmlpurifier_test.php b/lib/tests/htmlpurifier_test.php new file mode 100644 index 00000000000..a43b7d05580 --- /dev/null +++ b/lib/tests/htmlpurifier_test.php @@ -0,0 +1,185 @@ +. + +/** + * Unit tests for the HTMLPurifier integration + * + * @package core_core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +class core_htmlpurifier_testcase extends basic_testcase { + + /** + * Verify _blank target is allowed + * @return void + */ + public function test_allow_blank_target() { + $text = 'Some link'; + $result = format_text($text, FORMAT_HTML); + $this->assertSame($text, $result); + + $result = format_text('Some link', FORMAT_HTML); + $this->assertSame('Some link', $result); + } + + /** + * Verify our nolink tag accepted + * @return void + */ + public function test_nolink() { + // we can not use format text because nolink changes result + $text = '
no filters
'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + + $text = 'xxxxx
xxx
'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + } + + /** + * Verify our tex tag accepted + * @return void + */ + public function test_tex() { + $text = 'a+b=c'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + } + + /** + * Verify our algebra tag accepted + * @return void + */ + public function test_algebra() { + $text = 'a+b=c'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + } + + /** + * Verify our hacky multilang works + * @return void + */ + public function test_multilang() { + $text = 'hmmmhm'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + + $text = 'hmmmhm'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + + $text = 'hmmm'; + $result = purify_html($text, array()); + $this->assertNotSame($text, $result); + + // keep standard lang tags + + $text = 'asas'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + + $text = 'xxxxxx'; + $result = purify_html($text, array()); + $this->assertSame($text, $result); + } + + /** + * Tests the 'allowid' option for format_text. + * @return void + */ + public function test_format_text_allowid() { + // Start off by not allowing ids (default) + $options = array( + 'nocache' => true + ); + $result = format_text('
Frog
', FORMAT_HTML, $options); + $this->assertSame('
Frog
', $result); + + // Now allow ids + $options['allowid'] = true; + $result = format_text('
Frog
', FORMAT_HTML, $options); + $this->assertSame('
Frog
', $result); + } + + /** + * Test if linebreaks kept unchanged. + * @return void + */ + function test_line_breaking() { + $text = "\n\raa\rsss\nsss\r"; + $this->assertSame($text, purify_html($text)); + } + + /** + * Test fixing of strict problems. + * @return void + */ + function test_tidy() { + $text = "

xx"; + $this->assertSame('

xx

', purify_html($text)); + + $text = "

xx

"; + $this->assertSame('

xx

', purify_html($text)); + + $text = "xx
"; + $this->assertSame('xx
', purify_html($text)); + } + + /** + * Test nesting - this used to cause problems in earlier versions + * @return void + */ + function test_nested_lists() { + $text = ""; + $this->assertSame($text, purify_html($text)); + } + + /** + * Test that XSS protection works, complete smoke tests are in htmlpurifier itself. + * @return void + */ + function test_cleaning_nastiness() { + $text = "xx"; + $this->assertSame('xx', purify_html($text)); + + $text = '
xx
'; + $this->assertSame('
xx
', purify_html($text)); + + $text = '
xx
'; + $this->assertSame('
xx
', purify_html($text)); + + $text = 'xx'; + $this->assertSame('xx', purify_html($text)); + + $text = 'xx'; + $this->assertSame('xx', purify_html($text)); + + $text = 'xx'; + $this->assertSame('xx', purify_html($text)); + + $text = 'x
x'; + $this->assertSame('xx', purify_html($text)); + } +} + diff --git a/lib/tests/phpunit_test.php b/lib/tests/phpunit_test.php new file mode 100644 index 00000000000..90ec3daabe2 --- /dev/null +++ b/lib/tests/phpunit_test.php @@ -0,0 +1,45 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +/** + * Test integration of PHPUnit and custom Moodle hacks. + * + * @package core_core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_phpunit_basic_testcase extends basic_testcase { + + public function test_bootstrap() { + global $CFG; + $this->assertTrue(isset($CFG->httpswwwroot)); + $this->assertEquals($CFG->httpswwwroot, $CFG->wwwroot); + $this->assertEquals($CFG->prefix, $CFG->phpunit_prefix); + } + +/* + public function test_borked_tests() { + global $DB; + + $DB->set_field('user', 'confirmed', 1, array('id'=>-1)); + $CFG->xx = 'yy'; + } +*/ + +} \ No newline at end of file diff --git a/lib/tests/textlib_test.php b/lib/tests/textlib_test.php new file mode 100644 index 00000000000..9a7369149a9 --- /dev/null +++ b/lib/tests/textlib_test.php @@ -0,0 +1,420 @@ +. + +/** + * textlib unit teststest + * + * @package core_core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Unit tests for our utf-8 aware text processing + * + * @package core + * @subpackage lib + * @copyright 2010 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_textlib_testcase extends basic_testcase { + + public function test_parse_charset() { + $this->assertSame(textlib::parse_charset('Cp1250'), 'windows-1250'); + // does typo3 work? some encoding moodle does not use + $this->assertSame(textlib::parse_charset('ms-ansi'), 'windows-1252'); + } + + public function test_convert() { + $utf8 = "Žluťoučký koníček"; + $iso2 = pack("H*", "ae6c75bb6f75e86bfd206b6f6eede8656b"); + $win = pack("H*", "8e6c759d6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::convert($utf8, 'utf-8', 'iso-8859-2'), $iso2); + $this->assertSame(textlib::convert($iso2, 'iso-8859-2', 'utf-8'), $utf8); + $this->assertSame(textlib::convert($utf8, 'utf-8', 'win-1250'), $win); + $this->assertSame(textlib::convert($win, 'win-1250', 'utf-8'), $utf8); + $this->assertSame(textlib::convert($win, 'win-1250', 'iso-8859-2'), $iso2); + $this->assertSame(textlib::convert($iso2, 'iso-8859-2', 'win-1250'), $win); + $this->assertSame(textlib::convert($iso2, 'iso-8859-2', 'iso-8859-2'), $iso2); + $this->assertSame(textlib::convert($win, 'win-1250', 'cp1250'), $win); + + + $utf8 = '言語設定'; + $str = pack("H*", "b8c0b8ecc0dfc4ea"); //EUC-JP + $this->assertSame(textlib::convert($utf8, 'utf-8', 'EUC-JP'), $str); + $this->assertSame(textlib::convert($str, 'EUC-JP', 'utf-8'), $utf8); + + $str = pack("H*", "1b24423840386c405f446a1b2842"); //ISO-2022-JP + $this->assertSame(textlib::convert($utf8, 'utf-8', 'ISO-2022-JP'), $str); + $this->assertSame(textlib::convert($str, 'ISO-2022-JP', 'utf-8'), $utf8); + + $str = pack("H*", "8cbe8cea90dd92e8"); //SHIFT-JIS + $this->assertSame(textlib::convert($utf8, 'utf-8', 'SHIFT-JIS'), $str); + $this->assertSame(textlib::convert($str, 'SHIFT-JIS', 'utf-8'), $utf8); + + $utf8 = '简体中文'; + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB2312 + $this->assertSame(textlib::convert($utf8, 'utf-8', 'GB2312'), $str); + $this->assertSame(textlib::convert($str, 'GB2312', 'utf-8'), $utf8); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB18030 + $this->assertSame(textlib::convert($utf8, 'utf-8', 'GB18030'), $str); + $this->assertSame(textlib::convert($str, 'GB18030', 'utf-8'), $utf8); + } + + public function test_substr() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::substr($str, 0), $str); + $this->assertSame(textlib::substr($str, 1), 'luťoučký koníček'); + $this->assertSame(textlib::substr($str, 1, 3), 'luť'); + $this->assertSame(textlib::substr($str, 0, 100), $str); + $this->assertSame(textlib::substr($str, -3, 2), 'če'); + + $iso2 = pack("H*", "ae6c75bb6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::substr($iso2, 1, 3, 'iso-8859-2'), textlib::convert('luť', 'utf-8', 'iso-8859-2')); + $this->assertSame(textlib::substr($iso2, 0, 100, 'iso-8859-2'), textlib::convert($str, 'utf-8', 'iso-8859-2')); + $this->assertSame(textlib::substr($iso2, -3, 2, 'iso-8859-2'), textlib::convert('če', 'utf-8', 'iso-8859-2')); + + $win = pack("H*", "8e6c759d6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::substr($win, 1, 3, 'cp1250'), textlib::convert('luť', 'utf-8', 'cp1250')); + $this->assertSame(textlib::substr($win, 0, 100, 'cp1250'), textlib::convert($str, 'utf-8', 'cp1250')); + $this->assertSame(textlib::substr($win, -3, 2, 'cp1250'), textlib::convert('če', 'utf-8', 'cp1250')); + + + $str = pack("H*", "b8c0b8ecc0dfc4ea"); //EUC-JP + $s = pack("H*", "b8ec"); //EUC-JP + $this->assertSame(textlib::substr($str, 1, 1, 'EUC-JP'), $s); + + $str = pack("H*", "1b24423840386c405f446a1b2842"); //ISO-2022-JP + $s = pack("H*", "1b2442386c1b2842"); //ISO-2022-JP + $this->assertSame(textlib::substr($str, 1, 1, 'ISO-2022-JP'), $s); + + $str = pack("H*", "8cbe8cea90dd92e8"); //SHIFT-JIS + $s = pack("H*", "8cea"); //SHIFT-JIS + $this->assertSame(textlib::substr($str, 1, 1, 'SHIFT-JIS'), $s); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB2312 + $s = pack("H*", "cce5"); //GB2312 + $this->assertSame(textlib::substr($str, 1, 1, 'GB2312'), $s); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB18030 + $s = pack("H*", "cce5"); //GB18030 + $this->assertSame(textlib::substr($str, 1, 1, 'GB18030'), $s); + } + + public function test_strlen() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::strlen($str), 17); + + $iso2 = pack("H*", "ae6c75bb6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::strlen($iso2, 'iso-8859-2'), 17); + + $win = pack("H*", "8e6c759d6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::strlen($win, 'cp1250'), 17); + + + $str = pack("H*", "b8ec"); //EUC-JP + $this->assertSame(textlib::strlen($str, 'EUC-JP'), 1); + $str = pack("H*", "b8c0b8ecc0dfc4ea"); //EUC-JP + $this->assertSame(textlib::strlen($str, 'EUC-JP'), 4); + + $str = pack("H*", "1b2442386c1b2842"); //ISO-2022-JP + $this->assertSame(textlib::strlen($str, 'ISO-2022-JP'), 1); + $str = pack("H*", "1b24423840386c405f446a1b2842"); //ISO-2022-JP + $this->assertSame(textlib::strlen($str, 'ISO-2022-JP'), 4); + + $str = pack("H*", "8cea"); //SHIFT-JIS + $this->assertSame(textlib::strlen($str, 'SHIFT-JIS'), 1); + $str = pack("H*", "8cbe8cea90dd92e8"); //SHIFT-JIS + $this->assertSame(textlib::strlen($str, 'SHIFT-JIS'), 4); + + $str = pack("H*", "cce5"); //GB2312 + $this->assertSame(textlib::strlen($str, 'GB2312'), 1); + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB2312 + $this->assertSame(textlib::strlen($str, 'GB2312'), 4); + + $str = pack("H*", "cce5"); //GB18030 + $this->assertSame(textlib::strlen($str, 'GB18030'), 1); + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB18030 + $this->assertSame(textlib::strlen($str, 'GB18030'), 4); + } + + public function test_strtolower() { + $str = "Žluťoučký koníček"; + $low = 'žluťoučký koníček'; + $this->assertSame(textlib::strtolower($str), $low); + + $iso2 = pack("H*", "ae6c75bb6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::strtolower($iso2, 'iso-8859-2'), textlib::convert($low, 'utf-8', 'iso-8859-2')); + + $win = pack("H*", "8e6c759d6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::strtolower($win, 'cp1250'), textlib::convert($low, 'utf-8', 'cp1250')); + + + $str = '言語設定'; + $this->assertSame(textlib::strtolower($str), $str); + + $str = '简体中文'; + $this->assertSame(textlib::strtolower($str), $str); + + $str = pack("H*", "1b24423840386c405f446a1b2842"); //ISO-2022-JP + $this->assertSame(textlib::strtolower($str, 'ISO-2022-JP'), $str); + + $str = pack("H*", "8cbe8cea90dd92e8"); //SHIFT-JIS + $this->assertSame(textlib::strtolower($str, 'SHIFT-JIS'), $str); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB2312 + $this->assertSame(textlib::strtolower($str, 'GB2312'), $str); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB18030 + $this->assertSame(textlib::strtolower($str, 'GB18030'), $str); + } + + public function test_strtoupper() { + $str = "Žluťoučký koníček"; + $up = 'ŽLUŤOUČKÝ KONÍČEK'; + $this->assertSame(textlib::strtoupper($str), $up); + + $iso2 = pack("H*", "ae6c75bb6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::strtoupper($iso2, 'iso-8859-2'), textlib::convert($up, 'utf-8', 'iso-8859-2')); + + $win = pack("H*", "8e6c759d6f75e86bfd206b6f6eede8656b"); + $this->assertSame(textlib::strtoupper($win, 'cp1250'), textlib::convert($up, 'utf-8', 'cp1250')); + + + $str = '言語設定'; + $this->assertSame(textlib::strtoupper($str), $str); + + $str = '简体中文'; + $this->assertSame(textlib::strtoupper($str), $str); + + $str = pack("H*", "1b24423840386c405f446a1b2842"); //ISO-2022-JP + $this->assertSame(textlib::strtoupper($str, 'ISO-2022-JP'), $str); + + $str = pack("H*", "8cbe8cea90dd92e8"); //SHIFT-JIS + $this->assertSame(textlib::strtoupper($str, 'SHIFT-JIS'), $str); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB2312 + $this->assertSame(textlib::strtoupper($str, 'GB2312'), $str); + + $str = pack("H*", "bcf2cce5d6d0cec4"); //GB18030 + $this->assertSame(textlib::strtoupper($str, 'GB18030'), $str); + } + + public function test_strpos() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::strpos($str, 'koníč'), 10); + } + + public function test_strrpos() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::strrpos($str, 'o'), 11); + } + + public function test_specialtoascii() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::specialtoascii($str), 'Zlutoucky konicek'); + } + + public function test_encode_mimeheader() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::encode_mimeheader($str), '=?utf-8?B?xb1sdcWlb3XEjWvDvSBrb27DrcSNZWs=?='); + } + + public function test_entities_to_utf8() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::entities_to_utf8($str), "Žluťoučký koníček"); + } + + public function test_utf8_to_entities() { + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::utf8_to_entities($str), "Žluťoučký koníček"); + $this->assertSame(textlib::utf8_to_entities($str, true), "Žluťoučký koníček"); + + } + + public function test_trim_utf8_bom() { + $bom = "\xef\xbb\xbf"; + $str = "Žluťoučký koníček"; + $this->assertSame(textlib::trim_utf8_bom($bom.$str.$bom), $str.$bom); + } + + public function test_get_encodings() { + $encodings = textlib::get_encodings(); + $this->assertTrue(is_array($encodings)); + $this->assertTrue(count($encodings) > 1); + $this->assertTrue(isset($encodings['UTF-8'])); + } + + public function test_code2utf8() { + $this->assertSame(textlib::code2utf8(381), 'Ž'); + } + + public function test_strtotitle() { + $str = "žluťoučký koníček"; + $this->assertSame(textlib::strtotitle($str), "Žluťoučký Koníček"); + } + + public function test_deprecated_textlib_get_instance() { + ob_start(); + $textlib = textlib_get_instance(); + $output = ob_get_contents(); + $this->assertNotEmpty($output); + ob_end_clean(); + $this->assertSame($textlib->substr('abc', 1, 1), 'b'); + $this->assertSame($textlib->strlen('abc'), 3); + $this->assertSame($textlib->strtoupper('Abc'), 'ABC'); + $this->assertSame($textlib->strtolower('Abc'), 'abc'); + $this->assertSame($textlib->strpos('abc', 'a'), 0); + $this->assertSame($textlib->strpos('abc', 'd'), false); + $this->assertSame($textlib->strrpos('abcabc', 'a'), 3); + $this->assertSame($textlib->specialtoascii('ábc'), 'abc'); + $this->assertSame($textlib->strtotitle('abc ABC'), 'Abc Abc'); + } +} + + +/** + * Unit tests for our utf-8 aware collator. + * + * Used for sorting. + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collatorlib_testcase extends basic_testcase { + + protected $initiallang = null; + protected $error = null; + + public function setUp() { + global $SESSION; + if (isset($SESSION->lang)) { + $this->initiallang = $SESSION->lang; + } + $SESSION->lang = 'en'; // make sure we test en language to get consistent results, hopefully all systems have this locale + if (extension_loaded('intl')) { + $this->error = 'Collation aware sorting not supported'; + } else { + $this->error = 'Collation aware sorting not supported, PHP extension "intl" is not available.'; + } + parent::setUp(); + } + public function tearDown() { + global $SESSION; + parent::tearDown(); + if ($this->initiallang !== null) { + $SESSION->lang = $this->initiallang; + $this->initiallang = null; + } else { + unset($SESSION->lang); + } + } + function test_asort() { + $arr = array('b' => 'ab', 1 => 'aa', 0 => 'cc'); + collatorlib::asort($arr); + $this->assertSame(array_keys($arr), array(1, 'b', 0)); + $this->assertSame(array_values($arr), array('aa', 'ab', 'cc')); + + $arr = array('a' => 'áb', 'b' => 'ab', 1 => 'aa', 0=>'cc'); + collatorlib::asort($arr); + $this->assertSame(array_keys($arr), array(1, 'b', 'a', 0), $this->error); + $this->assertSame(array_values($arr), array('aa', 'ab', 'áb', 'cc'), $this->error); + } + function test_asort_objects_by_method() { + $objects = array( + 'b' => new string_test_class('ab'), + 1 => new string_test_class('aa'), + 0 => new string_test_class('cc') + ); + collatorlib::asort_objects_by_method($objects, 'get_protected_name'); + $this->assertSame(array_keys($objects), array(1, 'b', 0)); + $this->assertSame($this->get_ordered_names($objects, 'get_protected_name'), array('aa', 'ab', 'cc')); + + $objects = array( + 'a' => new string_test_class('áb'), + 'b' => new string_test_class('ab'), + 1 => new string_test_class('aa'), + 0 => new string_test_class('cc') + ); + collatorlib::asort_objects_by_method($objects, 'get_private_name'); + $this->assertSame(array_keys($objects), array(1, 'b', 'a', 0), $this->error); + $this->assertSame($this->get_ordered_names($objects, 'get_private_name'), array('aa', 'ab', 'áb', 'cc'), $this->error); + } + function test_asort_objects_by_property() { + $objects = array( + 'b' => new string_test_class('ab'), + 1 => new string_test_class('aa'), + 0 => new string_test_class('cc') + ); + collatorlib::asort_objects_by_property($objects, 'publicname'); + $this->assertSame(array_keys($objects), array(1, 'b', 0)); + $this->assertSame($this->get_ordered_names($objects, 'publicname'), array('aa', 'ab', 'cc')); + + $objects = array( + 'a' => new string_test_class('áb'), + 'b' => new string_test_class('ab'), + 1 => new string_test_class('aa'), + 0 => new string_test_class('cc') + ); + collatorlib::asort_objects_by_property($objects, 'publicname'); + $this->assertSame(array_keys($objects), array(1, 'b', 'a', 0), $this->error); + $this->assertSame($this->get_ordered_names($objects, 'publicname'), array('aa', 'ab', 'áb', 'cc'), $this->error); + } + protected function get_ordered_names($objects, $methodproperty = 'get_protected_name') { + $return = array(); + foreach ($objects as $object) { + if ($methodproperty == 'publicname') { + $return[] = $object->publicname; + } else { + $return[] = $object->$methodproperty(); + } + } + return $return; + } +} + + +/** + * Simple class used to work with the unit test. + * + * @package core + * @subpackage lib + * @copyright 2011 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class string_test_class extends stdClass { + public $publicname; + protected $protectedname; + private $privatename; + public function __construct($name) { + $this->publicname = $name; + $this->protectedname = $name; + $this->privatename = $name; + } + public function get_protected_name() { + return $this->protectedname; + } + public function get_private_name() { + return $this->publicname; + } +} diff --git a/lib/weblib.php b/lib/weblib.php index 7de357d4f5b..22141d27b4e 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -2742,7 +2742,10 @@ function debugging($message = '', $level = DEBUG_NORMAL, $backtrace = null) { $backtrace = debug_backtrace(); } $from = format_backtrace($backtrace, CLI_SCRIPT); - if (!empty($UNITTEST->running)) { + if (PHPUNITTEST) { + echo 'Debugging: ' . $message . "\n" . $from; + + } else if (!empty($UNITTEST->running)) { // When the unit tests are running, any call to trigger_error // is intercepted by the test framework and reported as an exception. // Therefore, we cannot use trigger_error during unit tests. diff --git a/mod/lti/simpletest/testlocallib.php b/mod/lti/simpletest/testlocallib.php index aaeeb42f6d3..62acc3f2b9d 100644 --- a/mod/lti/simpletest/testlocallib.php +++ b/mod/lti/simpletest/testlocallib.php @@ -50,6 +50,8 @@ defined('MOODLE_INTERNAL') || die; +global $CFG; + require_once($CFG->dirroot . '/mod/lti/locallib.php'); require_once($CFG->dirroot . '/mod/lti/servicelib.php'); diff --git a/mod/scorm/simpletest/test_formatduration.php b/mod/scorm/simpletest/test_formatduration.php index 2d2a808e1dc..cd8430ceaa7 100644 --- a/mod/scorm/simpletest/test_formatduration.php +++ b/mod/scorm/simpletest/test_formatduration.php @@ -20,6 +20,8 @@ if (!defined('MOODLE_INTERNAL')) { // Unit tests for scorm_formatduration function from locallib.php +global $CFG; + // Make sure the code being tested is accessible. require_once($CFG->dirroot . '/mod/scorm/locallib.php'); // Include the code to test diff --git a/mod/url/locallib.php b/mod/url/locallib.php index 70e7efc1b9e..070f0e59413 100644 --- a/mod/url/locallib.php +++ b/mod/url/locallib.php @@ -40,9 +40,9 @@ require_once("$CFG->dirroot/mod/url/lib.php"); function url_appears_valid_url($url) { if (preg_match('/^(\/|https?:|ftp:)/i', $url)) { // note: this is not exact validation, we look for severely malformed URLs only - return preg_match('/^[a-z]+:\/\/([^:@\s]+:[^@\s]+@)?[a-z0-9_\.\-]+(:[0-9]+)?(\/[^#]*)?(#.*)?$/i', $url); + return (bool)preg_match('/^[a-z]+:\/\/([^:@\s]+:[^@\s]+@)?[a-z0-9_\.\-]+(:[0-9]+)?(\/[^#]*)?(#.*)?$/i', $url); } else { - return preg_match('/^[a-z]+:\/\/...*$/i', $url); + return (bool)preg_match('/^[a-z]+:\/\/...*$/i', $url); } } diff --git a/mod/url/simpletest/testlib.php b/mod/url/simpletest/testlib.php index 2dbf1bab845..18a026edab2 100644 --- a/mod/url/simpletest/testlib.php +++ b/mod/url/simpletest/testlib.php @@ -26,6 +26,8 @@ defined('MOODLE_INTERNAL') || die(); +global $CFG; + require_once($CFG->dirroot . '/mod/url/locallib.php'); diff --git a/mod/url/tests/lib_test.php b/mod/url/tests/lib_test.php new file mode 100644 index 00000000000..752bc677ad8 --- /dev/null +++ b/mod/url/tests/lib_test.php @@ -0,0 +1,67 @@ +. + +/** + * Unit tests for some mod URL lib stuff. + * + * @package mod_url + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * mod_url tests + * + * @package mod_url + * @category phpunit + * @copyright 2011 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_url_lib_testcase extends basic_testcase { + + public static function setUpBeforeClass() { + global $CFG; + require_once($CFG->dirroot . '/mod/url/locallib.php'); + } + + public function test_url_appears_valid_url() { + $this->assertTrue(url_appears_valid_url('http://example')); + $this->assertTrue(url_appears_valid_url('http://www.example.com')); + $this->assertTrue(url_appears_valid_url('http://www.exa-mple2.com')); + $this->assertTrue(url_appears_valid_url('http://www.example.com/~nobody/index.html')); + $this->assertTrue(url_appears_valid_url('http://www.example.com#hmm')); + $this->assertTrue(url_appears_valid_url('http://www.example.com/#hmm')); + $this->assertTrue(url_appears_valid_url('http://www.example.com/žlutý koníček/lala.txt')); + $this->assertTrue(url_appears_valid_url('http://www.example.com/žlutý koníček/lala.txt#hmmmm')); + $this->assertTrue(url_appears_valid_url('http://www.example.com/index.php?xx=yy&zz=aa')); + $this->assertTrue(url_appears_valid_url('https://user:password@www.example.com/žlutý koníček/lala.txt')); + $this->assertTrue(url_appears_valid_url('ftp://user:password@www.example.com/žlutý koníček/lala.txt')); + + $this->assertFalse(url_appears_valid_url('http:example.com')); + $this->assertFalse(url_appears_valid_url('http:/example.com')); + $this->assertFalse(url_appears_valid_url('http://')); + $this->assertFalse(url_appears_valid_url('http://www.exa mple.com')); + $this->assertFalse(url_appears_valid_url('http://www.examplé.com')); + $this->assertFalse(url_appears_valid_url('http://@www.example.com')); + $this->assertFalse(url_appears_valid_url('http://user:@www.example.com')); + + $this->assertTrue(url_appears_valid_url('lalala://@:@/')); + } +} \ No newline at end of file diff --git a/mod/wiki/simpletest/testwikiparser.php b/mod/wiki/simpletest/testwikiparser.php index 9d197abbd95..024642e0ded 100644 --- a/mod/wiki/simpletest/testwikiparser.php +++ b/mod/wiki/simpletest/testwikiparser.php @@ -35,6 +35,8 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } +global $CFG; + require_once($CFG->dirroot . '/mod/wiki/parser/parser.php'); class wikiparser_test extends UnitTestCase { diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000000..09456e10c22 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + lib/tests + + + + + + + +