MDL-45513 cache: implemented defines for unit testing alternative caches
authorSam Hemelryk <sam@moodle.com>
Mon, 19 May 2014 00:00:59 +0000 (12:00 +1200)
committerSam Hemelryk <sam@moodle.com>
Thu, 29 May 2014 20:41:07 +0000 (08:41 +1200)
cache/README.md
cache/classes/factory.php
cache/classes/store.php
cache/stores/memcache/lib.php
cache/stores/memcache/tests/memcache_test.php
cache/stores/memcached/lib.php
cache/stores/memcached/tests/memcached_test.php
cache/stores/mongodb/lib.php
cache/stores/mongodb/tests/mongodb_test.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php

index 7e9cd59..7745f0e 100644 (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.
index b9b42f4..6cf975f 100644 (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;
index ab22575..22c20db 100644 (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);
+    }
 }
index 21e8f6c..42db75b 100644 (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
index 4e3c2aa..62b52f4 100644 (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();
index 2c601fe..1570a70 100644 (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
index 80a918a..a6c3496 100644 (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();
index f30323d..db8653f 100644 (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
index b1a3390..97bfb9d 100644 (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
index 7d8fe16..9e00372 100644 (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.');
index 845ca41..0e662cb 100644 (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);
+    }
 }
\ No newline at end of file