MDL-45513 cache: implemented defines for unit testing alternative caches

This commit is contained in:
Sam Hemelryk
2014-05-30 08:41:07 +12:00
parent 79a8ea653c
commit eefb680d8e
11 changed files with 290 additions and 61 deletions
+20
View File
@@ -246,3 +246,23 @@ The following snippet illustates how to configure the three core cache stores th
define('TEST_CACHESTORE_MEMCACHE_TESTSERVERS', '127.0.0.1:11211');
define('TEST_CACHESTORE_MEMCACHED_TESTSERVERS', '127.0.0.1:11211');
define('TEST_CACHESTORE_MONGODB_TESTSERVER', 'mongodb://localhost:27017');
As of Moodle 2.8 it is also possible to set the default cache stores used when running unit tests.
You can do this by adding the following define to your config.php file:
// xxx is one of Memcache, Memecached, mongodb or other cachestore with a test define.
define('TEST_CACHE_USING_APPLICATION_STORE', 'xxx');
This allows you to run tests against a defined test store. It uses the defined value to identify a store to test against with a matching TEST_CACHESTORE define.
Alternatively you can also run unit tests against an actual cache config.
To do this you must add the following to your config.php file:
define('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH', true');
$CFG->altcacheconfigpath = '/a/temp/directory/yoursite.php'
This tells Moodle to use the config at $CFG->altcacheconfigpath when running unit tests.
There are a couple of considerations to using this method:
* By setting $CFG->altcacheconfigpath your site will store the cache config in the specified path, not just the unit test cache config but your site config as well.
* If you have configured your cache before setting $CFG->altcacheconfigpath you will need to copy it from moodledata/muc/config.php to the destination you specified.
* This allows you to share a cache config between sites.
* It also allows you to use unit tests to test your sites cache config.
+14 -12
View File
@@ -126,6 +126,14 @@ class cache_factory {
// situation. It will use disabled alternatives where available.
require_once($CFG->dirroot.'/cache/disabledlib.php');
self::$instance = new cache_factory_disabled();
} else if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
// We're using the regular factory.
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
self::$instance = new cache_phpunit_factory();
if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) {
// The cache stores have been disabled.
self::$instance->set_state(self::STATE_STORES_DISABLED);
}
} else {
// We're using the regular factory.
self::$instance = new cache_factory();
@@ -327,8 +335,10 @@ class cache_factory {
// The class to use.
$class = 'cache_config';
$unittest = defined('PHPUNIT_TEST') && PHPUNIT_TEST;
// Check if this is a PHPUnit test and redirect to the phpunit config classes if it is.
if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
if ($unittest) {
require_once($CFG->dirroot.'/cache/locallib.php');
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
// We have just a single class for PHP unit tests. We don't care enough about its
@@ -342,17 +352,9 @@ class cache_factory {
if ($writer || $needtocreate) {
require_once($CFG->dirroot.'/cache/locallib.php');
$class .= '_writer';
}
// Check if this is a PHPUnit test and redirect to the phpunit config classes if it is.
if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
require_once($CFG->dirroot.'/cache/locallib.php');
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
// We have just a single class for PHP unit tests. We don't care enough about its
// performance to do otherwise and having a single method allows us to inject things into it
// while testing.
$class = 'cache_config_phpunittest';
if (!$unittest) {
$class .= '_writer';
}
}
$error = false;
+25
View File
@@ -79,6 +79,17 @@ interface cache_store_interface {
* @return cache_store|false
*/
public static function initialise_test_instance(cache_definition $definition);
/**
* Initialises a test instance for unit tests.
*
* This differs from initialise_test_instance in that it doesn't rely on interacting with the config table.
*
* @since 2.8
* @param cache_definition $definition
* @return cache_store|false
*/
public static function initialise_unit_test_instance(cache_definition $definition);
}
/**
@@ -340,4 +351,18 @@ abstract class cache_store implements cache_store_interface {
// Any stores that have an issue with this will need to override the create_clone method.
return clone($this);
}
/**
* Initialises a test instance for unit tests.
*
* This differs from initialise_test_instance in that it doesn't rely on interacting with the config table.
* By default however it calls initialise_test_instance to support backwards compatability.
*
* @since 2.8
* @param cache_definition $definition
* @return cache_store|false
*/
public static function initialise_unit_test_instance(cache_definition $definition) {
return self::initialise_test_instance($definition);
}
}
+21
View File
@@ -430,6 +430,27 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
return $store;
}
/**
* Creates a test instance for unit tests if possible.
* @param cache_definition $definition
* @return bool|cachestore_memcache
*/
public static function initialise_unit_test_instance(cache_definition $definition) {
if (!self::are_requirements_met()) {
return false;
}
if (!defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
return false;
}
$configuration = array();
$configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHE_TESTSERVERS);
$store = new cachestore_memcache('Test memcache', $configuration);
$store->initialise($definition);
return $store;
}
/**
* Returns the name of this instance.
* @return string
+1 -11
View File
@@ -42,16 +42,6 @@ require_once($CFG->dirroot.'/cache/stores/memcache/lib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_memcache_test extends cachestore_tests {
/**
* Prepare to run tests.
*/
public function setUp() {
if (defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
set_config('testservers', TEST_CACHESTORE_MEMCACHE_TESTSERVERS, 'cachestore_memcache');
$this->resetAfterTest();
}
parent::setUp();
}
/**
* Returns the memcache class name
* @return string
@@ -65,7 +55,7 @@ class cachestore_memcache_test extends cachestore_tests {
*/
public function test_valid_keys() {
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
$instance = cachestore_memcache::initialise_test_instance($definition);
$instance = cachestore_memcache::initialise_unit_test_instance($definition);
if (!$instance) { // Something prevented memcache store to be inited (extension, TEST_CACHESTORE_MEMCACHE_TESTSERVERS...).
$this->markTestSkipped();
+22
View File
@@ -487,6 +487,28 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
return $store;
}
/**
* Creates a test instance for unit tests if possible.
* @param cache_definition $definition
* @return bool|cachestore_memcached
*/
public static function initialise_unit_test_instance(cache_definition $definition) {
if (!self::are_requirements_met()) {
return false;
}
if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
return false;
}
$configuration = array();
$configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS);
$store = new cachestore_memcached('Test memcached', $configuration);
$store->initialise($definition);
return $store;
}
/**
* Returns the name of this instance.
* @return string
+1 -11
View File
@@ -42,16 +42,6 @@ require_once($CFG->dirroot.'/cache/stores/memcached/lib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_memcached_test extends cachestore_tests {
/**
* Prepare to run tests.
*/
public function setUp() {
if (defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
set_config('testservers', TEST_CACHESTORE_MEMCACHED_TESTSERVERS, 'cachestore_memcached');
$this->resetAfterTest();
}
parent::setUp();
}
/**
* Returns the memcached class name
* @return string
@@ -65,7 +55,7 @@ class cachestore_memcached_test extends cachestore_tests {
*/
public function test_valid_keys() {
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
$instance = cachestore_memcached::initialise_test_instance($definition);
$instance = cachestore_memcached::initialise_unit_test_instance($definition);
if (!$instance) { // Something prevented memcached store to be inited (extension, TEST_CACHESTORE_MEMCACHED_TESTSERVERS...).
$this->markTestSkipped();
+24
View File
@@ -561,6 +561,30 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
return $store;
}
/**
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
* @return false
*/
public static function initialise_unit_test_instance(cache_definition $definition) {
if (!self::are_requirements_met()) {
return false;
}
if (!defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
return false;
}
$configuration = array();
$configuration['servers'] = explode("\n", TEST_CACHESTORE_MONGODB_TESTSERVER);
$store = new cachestore_mongodb('Test mongodb', $configuration);
$store->initialise($definition);
return $store;
}
/**
* Returns the name of this instance.
* @return string
-10
View File
@@ -42,16 +42,6 @@ require_once($CFG->dirroot.'/cache/stores/mongodb/lib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_mongodb_test extends cachestore_tests {
/**
* Prepare to run tests.
*/
public function setUp() {
if (defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
set_config('testserver', TEST_CACHESTORE_MONGODB_TESTSERVER, 'cachestore_mongodb');
$this->resetAfterTest();
}
parent::setUp();
}
/**
* Returns the MongoDB class name
* @return string
+27 -6
View File
@@ -58,18 +58,37 @@ class core_cache_testcase extends advanced_testcase {
cache_factory::reset();
}
/**
* Returns the expected application cache store.
* @return string
*/
protected function get_expected_application_cache_store() {
$expected = 'cachestore_file';
if (defined('TEST_CACHE_USING_APPLICATION_STORE') && preg_match('#[a-zA-Z][a-zA-Z0-9_]*#', TEST_CACHE_USING_APPLICATION_STORE)) {
$expected = 'cachestore_'.(string)TEST_CACHE_USING_APPLICATION_STORE;
}
return $expected;
}
/**
* Tests cache configuration
*/
public function test_cache_config() {
global $CFG;
if (!empty($CFG->altcacheconfigpath)) {
if (defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') && TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH &&
!empty($CFG->altcacheconfigpath)) {
// We need to skip this test - it checks the default config structure, but very likely we arn't using the
// default config structure here so theres no point in running the test.
$this->markTestSkipped('Skipped testing default cache config structure as alt cache path is being used.');
}
if (defined('TEST_CACHE_USING_APPLICATION_STORE')) {
// We need to skip this test - it checks the default config structure, but very likely we arn't using the
// default config structure here because we are testing against an alternative application store.
$this->markTestSkipped('Skipped testing default cache config structure as alt application store is being used.');
}
$instance = cache_config::instance();
$this->assertInstanceOf('cache_config_phpunittest', $instance);
@@ -482,27 +501,29 @@ class core_cache_testcase extends advanced_testcase {
* Test the mappingsonly setting.
*/
public function test_definition_mappings_only() {
/** @var cache_config_phpunittest $instance */
$instance = cache_config_phpunittest::instance(true);
$instance->phpunit_add_definition('phpunit/mappingsonly', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'mappingsonly',
'mappingsonly' => true
));
), false);
$instance->phpunit_add_definition('phpunit/nonmappingsonly', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'nonmappingsonly',
'mappingsonly' => false
));
), false);
$cacheonly = cache::make('phpunit', 'mappingsonly');
$this->assertInstanceOf('cache_application', $cacheonly);
$this->assertEquals('cachestore_dummy', $cacheonly->phpunit_get_store_class());
$expected = $this->get_expected_application_cache_store();
$cachenon = cache::make('phpunit', 'nonmappingsonly');
$this->assertInstanceOf('cache_application', $cachenon);
$this->assertEquals('cachestore_file', $cachenon->phpunit_get_store_class());
$this->assertEquals($expected, $cachenon->phpunit_get_store_class());
}
/**
@@ -1082,7 +1103,7 @@ class core_cache_testcase extends advanced_testcase {
*/
public function test_alt_cache_path() {
global $CFG;
if ($CFG->altcacheconfigpath) {
if ((defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') && TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH) || !empty($CFG->altcacheconfigpath)) {
$this->markTestSkipped('Skipped testing alt cache path as it is already being used.');
}
$this->resetAfterTest();
@@ -1161,7 +1182,7 @@ class core_cache_testcase extends advanced_testcase {
public function test_disable() {
global $CFG;
if (!empty($CFG->altcacheconfigpath)) {
if ((defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') && TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH) || !empty($CFG->altcacheconfigpath)) {
// We can't run this test as it requires us to delete the cache configuration script which we just
// cant do with a custom path in play.
$this->markTestSkipped('Skipped testing cache disable functionality as alt cache path is being used.');
+135 -11
View File
@@ -28,6 +28,8 @@
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/cache/locallib.php');
/**
* Override the default cache configuration for our own maniacle purposes.
*
@@ -36,6 +38,86 @@ defined('MOODLE_INTERNAL') || die();
*/
class cache_config_phpunittest extends cache_config_writer {
/**
* Creates the default configuration and saves it.
*
* This function calls config_save, however it is safe to continue using it afterwards as this function should only ever
* be called when there is no configuration file already.
*
* @param bool $forcesave If set to true then we will forcefully save the default configuration file.
* @return true|array Returns true if the default configuration was successfully created.
* Returns a configuration array if it could not be saved. This is a bad situation. Check your error logs.
*/
public static function create_default_configuration($forcesave = false) {
global $CFG;
// HACK ALERT.
// We probably need to come up with a better way to create the default stores, or at least ensure 100% that the
// default store plugins are protected from deletion.
$writer = new self;
$writer->configstores = self::get_default_stores();
$writer->configdefinitions = self::locate_definitions();
$defaultapplication = 'default_application';
$appdefine = defined('TEST_CACHE_USING_APPLICATION_STORE') ? TEST_CACHE_USING_APPLICATION_STORE : false;
if ($appdefine !== false && preg_match('/^[a-zA-Z][a-zA-Z0-9_]+$/', $appdefine)) {
$expectedstore = $appdefine;
$expecteddefine = 'TEST_CACHESTORE_'.strtoupper($expectedstore).'_TESTSERVERS';
$file = $CFG->dirroot.'/cache/stores/'.$appdefine.'/lib.php';
$class = 'cachestore_'.$appdefine;
if (file_exists($file)) {
require_once($file);
}
if (defined($expecteddefine) && class_exists($class)) {
/** @var cache_store $class */
$writer->configstores['test_application'] = array(
'use_test_store' => true,
'name' => 'test_application',
'plugin' => $expectedstore,
'alt' => $writer->configstores[$defaultapplication],
'modes' => $class::get_supported_modes(),
'features' => $class::get_supported_features()
);
$defaultapplication = 'test_application';
}
}
$writer->configmodemappings = array(
array(
'mode' => cache_store::MODE_APPLICATION,
'store' => $defaultapplication,
'sort' => -1
),
array(
'mode' => cache_store::MODE_SESSION,
'store' => 'default_session',
'sort' => -1
),
array(
'mode' => cache_store::MODE_REQUEST,
'store' => 'default_request',
'sort' => -1
)
);
$writer->configlocks = array(
'default_file_lock' => array(
'name' => 'cachelock_file_default',
'type' => 'cachelock_file',
'dir' => 'filelocks',
'default' => true
)
);
$factory = cache_factory::instance();
// We expect the cache to be initialising presently. If its not then something has gone wrong and likely
// we are now in a loop.
if (!$forcesave && $factory->get_state() !== cache_factory::STATE_INITIALISING) {
return $writer->generate_configuration_array();
}
$factory->set_state(cache_factory::STATE_SAVING);
$writer->config_save();
return true;
}
/**
* Returns the expected path to the configuration file.
*
@@ -53,6 +135,14 @@ class cache_config_phpunittest extends cache_config_writer {
$configpath = $CFG->dataroot.'/muc/config.php';
if (!empty($CFG->altcacheconfigpath)) {
if (defined('PHPUNIT_TEST') && PHPUNIT_TEST &&
(!defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') || !TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH)) {
// We're within a unit test, but TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH has not being defined or is
// false, we want to use the default.
return $configpath;
}
$path = $CFG->altcacheconfigpath;
if (is_dir($path) && is_writable($path)) {
// Its a writable directory, thats fine. Convert it to a file.
@@ -81,8 +171,10 @@ class cache_config_phpunittest extends cache_config_writer {
* Adds a definition to the stack
* @param string $area
* @param array $properties
* @param bool $addmapping By default this method adds a definition and a mapping for that definition. You can
* however set this to false if you only want it to add the definition and not the mapping.
*/
public function phpunit_add_definition($area, array $properties) {
public function phpunit_add_definition($area, array $properties, $addmapping = true) {
if (!array_key_exists('overrideclass', $properties)) {
switch ($properties['mode']) {
case cache_store::MODE_APPLICATION:
@@ -97,16 +189,18 @@ class cache_config_phpunittest extends cache_config_writer {
}
}
$this->configdefinitions[$area] = $properties;
switch ($properties['mode']) {
case cache_store::MODE_APPLICATION:
$this->phpunit_add_definition_mapping($area, 'default_application', 0);
break;
case cache_store::MODE_SESSION:
$this->phpunit_add_definition_mapping($area, 'default_session', 0);
break;
case cache_store::MODE_REQUEST:
$this->phpunit_add_definition_mapping($area, 'default_request', 0);
break;
if ($addmapping) {
switch ($properties['mode']) {
case cache_store::MODE_APPLICATION:
$this->phpunit_add_definition_mapping($area, 'default_application', 0);
break;
case cache_store::MODE_SESSION:
$this->phpunit_add_definition_mapping($area, 'default_session', 0);
break;
case cache_store::MODE_REQUEST:
$this->phpunit_add_definition_mapping($area, 'default_request', 0);
break;
}
}
}
@@ -395,4 +489,34 @@ class cache_phpunit_factory extends cache_factory {
public static function phpunit_disable() {
parent::disable();
}
/**
* Creates a store instance given its name and configuration.
*
* If the store has already been instantiated then the original object will be returned. (reused)
*
* @param string $name The name of the store (must be unique remember)
* @param array $details
* @param cache_definition $definition The definition to instantiate it for.
* @return boolean|cache_store
*/
public function create_store_from_config($name, array $details, cache_definition $definition) {
if (isset($details['use_test_store'])) {
// name, plugin, alt
$class = 'cachestore_'.$details['plugin'];
$method = 'initialise_unit_test_instance';
if (class_exists($class) && method_exists($class, $method)) {
$instance = $class::$method($definition);
if ($instance) {
return $instance;
}
}
$details = $details['alt'];
$name = $details['name'];
}
return parent::create_store_from_config($name, $details, $definition);
}
}