You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by rj...@apache.org on 2014/01/30 04:46:39 UTC
svn commit: r1562687 - in /bloodhound/trunk/bloodhound_multiproduct:
multiproduct/config.py multiproduct/env.py multiproduct/hooks.py
tests/config.py tests/env.py
Author: rjollos
Date: Thu Jan 30 03:46:39 2014
New Revision: 1562687
URL: http://svn.apache.org/r1562687
Log:
0.8dev: Changes to the product configuration will take effect immediately. Refs #539.
Patch by Olemis Lang.
Modified:
bloodhound/trunk/bloodhound_multiproduct/multiproduct/config.py
bloodhound/trunk/bloodhound_multiproduct/multiproduct/env.py
bloodhound/trunk/bloodhound_multiproduct/multiproduct/hooks.py
bloodhound/trunk/bloodhound_multiproduct/tests/config.py
bloodhound/trunk/bloodhound_multiproduct/tests/env.py
Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/config.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/config.py?rev=1562687&r1=1562686&r2=1562687&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/multiproduct/config.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/config.py Thu Jan 30 03:46:39 2014
@@ -25,6 +25,7 @@ import os.path
from trac.config import Configuration, ConfigurationError, Option, \
OrderedExtensionsOption, Section, _use_default
from trac.resource import ResourceNotFound
+from trac.util import create_file
from trac.util.text import to_unicode
from multiproduct.model import ProductSetting
@@ -37,6 +38,9 @@ class Configuration(Configuration):
Python Standard Library) but retrieving configuration values
from the database.
"""
+
+ CONFIG_LOCK_FILE = 'config.lock'
+
def __init__(self, env, product, parents=None):
"""Initialize configuration object with an instance of
`trac.env.Environment` and product prefix.
@@ -48,6 +52,11 @@ class Configuration(Configuration):
self.env = env
self.product = to_unicode(product)
self._sections = {}
+ self._lastmtime = 0
+ self._lock_path = os.path.join(self.env.path, self.CONFIG_LOCK_FILE)
+ if not os.path.exists(self._lock_path):
+ create_file(self._lock_path)
+ self._orig_parents = parents
self._setup_parents(parents)
def __getitem__(self, name):
@@ -57,6 +66,10 @@ class Configuration(Configuration):
self._sections[name] = Section(self, name)
return self._sections[name]
+ def get_lock_file_mtime(self):
+ """Returns to modification time of the lock file."""
+ return os.path.getmtime(self._lock_path)
+
def sections(self, compmgr=None, defaults=True):
"""Return a list of section names.
@@ -87,25 +100,40 @@ class Configuration(Configuration):
return defaults and (section, option) in Option.registry
def save(self):
- """Nothing to do.
+ """Just touch config lock file.
- Notice: Opposite to Trac's Configuration objects Bloodhound's
+ Notice: In contrast to Trac's Configuration objects Bloodhound's
product configuration objects commit changes to the database
immediately. Thus there's no much to do in this method.
"""
+ self.touch()
+ self._lastmtime = self.get_lock_file_mtime()
def parse_if_needed(self, force=False):
- """Just invalidate options cache.
+ """Invalidate options cache considering global lock timestamp.
Notice: Opposite to Trac's Configuration objects Bloodhound's
product configuration objects commit changes to the database
immediately. Thus there's no much to do in this method.
"""
- for section in self.sections():
- self[section]._cache.clear()
+ changed = False
+ modtime = self.get_lock_file_mtime()
+ if force or modtime > self._lastmtime:
+ self._sections = {}
+ self._lastmtime = modtime
+ changed = True
+
+ if changed:
+ self._setup_parents(self._orig_parents)
+ else:
+ for parent in self.parents:
+ changed |= parent.parse_if_needed(force=force)
+
+ return changed
def touch(self):
- pass
+ if os.access(self._lock_path, os.W_OK):
+ os.utime(self._lock_path, None)
def set_defaults(self, compmgr=None):
"""Retrieve all default values and store them explicitly in the
Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/env.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/env.py?rev=1562687&r1=1562686&r2=1562687&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/multiproduct/env.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/env.py Thu Jan 30 03:46:39 2014
@@ -323,8 +323,7 @@ class EnvironmentStub(trac.test.Environm
del env._rules
except AttributeError:
pass
- # FIXME: Shall we ?
- #env.config.save()
+ env.config.save()
def reset_db(self, default_data=None):
multiproduct_schema = self._multiproduct_schema_enabled
@@ -634,6 +633,25 @@ class ProductEnvironment(Component, Comp
is_component_enabled_local = trac.env.Environment.is_component_enabled.im_func
+ def is_enabled(self, cls):
+ """Return whether the given component class is enabled."""
+ modtime = max(self.config.get_lock_file_mtime(),
+ self.config._lastmtime)
+ if modtime > self._config_mtime:
+ self.enabled.clear()
+ try:
+ del self._rules
+ except AttributeError:
+ pass
+ # FIXME : Improve cache hits by tracking global env last mtime
+ self.parent.enabled.clear()
+ try:
+ del self.parent._rules
+ except AttributeError:
+ pass
+ self._config_mtime = modtime
+ return super(ProductEnvironment, self).is_enabled(cls)
+
def is_component_enabled(self, cls):
"""Implemented to only allow activation of components already
activated in the global environment that are in turn not disabled in
@@ -806,6 +824,7 @@ class ProductEnvironment(Component, Comp
else:
parents = [self.parent.config]
self.config = Configuration(self.parent, self.product.prefix, parents)
+ self._config_mtime = 0
self.setup_log()
def setup_log(self):
Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/hooks.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/hooks.py?rev=1562687&r1=1562686&r2=1562687&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/multiproduct/hooks.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/hooks.py Thu Jan 30 03:46:39 2014
@@ -70,6 +70,7 @@ class MultiProductEnvironmentFactory(Env
env = create_product_env(pid,
environ['SCRIPT_NAME'] + '/products/' +
pid, product_path)
+ env.config.parse_if_needed()
return env
Modified: bloodhound/trunk/bloodhound_multiproduct/tests/config.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/tests/config.py?rev=1562687&r1=1562686&r2=1562687&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/tests/config.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/tests/config.py Thu Jan 30 03:46:39 2014
@@ -19,12 +19,13 @@
"""Tests for Apache(TM) Bloodhound's product configuration objects"""
-from ConfigParser import ConfigParser
-from itertools import groupby
import os.path
import shutil
-from StringIO import StringIO
+import time
import unittest
+from ConfigParser import ConfigParser
+from StringIO import StringIO
+from itertools import groupby
from trac.config import Option
from trac.tests.config import ConfigurationTestCase
@@ -35,9 +36,8 @@ from multiproduct.config import Configur
from multiproduct.model import Product, ProductSetting
from tests.env import MultiproductTestCase
-class ProductConfigTestCase(ConfigurationTestCase, MultiproductTestCase):
- r"""Test cases for Trac configuration objects rewritten for product
- scope.
+class MultiproductConfigTestCase(MultiproductTestCase):
+ r"""Test setup for configuration test cases.
"""
def setUp(self):
r"""Replace Trac environment with product environment
@@ -48,7 +48,7 @@ class ProductConfigTestCase(Configuratio
tmpdir = os.path.realpath(self.env.path)
self.filename = os.path.join(tmpdir, 'conf', 'product.ini')
# Ensure conf sub-folder is created
- os.mkdir(os.path.dirname(self.filename))
+ os.path.dirname(self.filename)
self._upgrade_mp(self.env)
self._setup_test_log(self.env)
@@ -131,6 +131,11 @@ class ProductConfigTestCase(Configuratio
dump.append('%s = %s\n' % (row[1], row[2]))
return dump
+
+class ProductConfigTestCase(MultiproductConfigTestCase, ConfigurationTestCase):
+ r"""Test cases for Trac configuration objects rewritten for product
+ scope.
+ """
# Test cases rewritten to avoid reading config file.
# It does make sense for product config as it's stored in the database
@@ -196,8 +201,62 @@ class ProductConfigTestCase(Configuratio
config.set('a', 'option', 'value2')
self.assertEquals('value2', config.get('a', 'option'))
+
+class ProductConfigSyncTestCase(MultiproductConfigTestCase):
+ """Test cases for concurrent access of product configuration objects.
+ """
+ def test_sync(self):
+ """Config cache consistency on concurrent edits
+ """
+ config1 = self._read()
+ config2 = self._read()
+
+ # Initial values will be empty
+ # This will initialize both instances' cache
+ self.assertEqual('', config1.get('s', 'o'))
+ self.assertEqual('', config2.get('s', 'o'))
+
+ # First time assignment, no actual cache
+ config1.set('s', 'o', 'value0')
+ self.assertEqual('value0', config1.get('s', 'o'))
+ self.assertEqual('value0', config2.get('s', 'o'))
+
+ # Subsequent hits retrieved from cache
+ config1.set('s', 'o', 'value1')
+ self.assertEqual('value0', config2.get('s', 'o'))
+ # ... unless cache invalidated e.g. by calling save()
+ config1.save()
+ self.assertTrue(config2.parse_if_needed())
+ self.assertEqual('value1', config1.get('s', 'o'))
+ self.assertEqual('value1', config2.get('s', 'o'))
+
+ # TODO: Replace with trac.util.compat:wait_for_file_mtime_change when
+ # changes from Trac 1.0-stable (> r12258) or Trac 1.0.2 are integrated
+ # Two edits may look simultaneous depending on FS accuracy,
+ # so wait 1 second to ensure next timestamp below will be different
+ # otherwise the test is fragile and results non-deterministic.
+ # This holds for Trac config objects too.
+ time.sleep(1)
+
+ # After update no subsequent modifications reported
+ config2.set('s', 'o', 'value2')
+ self.assertFalse(config1.parse_if_needed())
+ self.assertEqual('value1', config1.get('s', 'o'))
+ # ... unless cache invalidated e.g. by calling touch()
+ config2.touch()
+ self.assertTrue(config1.parse_if_needed())
+ self.assertEqual('value2', config1.get('s', 'o'))
+ self.assertEqual('value2', config2.get('s', 'o'))
+ self.assertTrue(config2.parse_if_needed())
+
+
def test_suite():
- return unittest.makeSuite(ProductConfigTestCase,'test')
+ suite = unittest.TestSuite()
+
+ suite.addTest(unittest.makeSuite(ProductConfigTestCase,'test'))
+ suite.addTest(unittest.makeSuite(ProductConfigSyncTestCase,'test'))
+
+ return suite
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Modified: bloodhound/trunk/bloodhound_multiproduct/tests/env.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/tests/env.py?rev=1562687&r1=1562686&r2=1562687&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/tests/env.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/tests/env.py Thu Jan 30 03:46:39 2014
@@ -19,15 +19,17 @@
"""Tests for Apache(TM) Bloodhound's product environments"""
-from inspect import stack
import os.path
import shutil
+import sys
import tempfile
+from inspect import stack
from tests import unittest
from types import MethodType
+from trac.admin.api import AdminCommandManager, IAdminCommandProvider
from trac.config import Option
-from trac.core import Component, ComponentMeta
+from trac.core import Component, ComponentMeta, implements
from trac.env import Environment
from trac.test import EnvironmentStub, MockPerm
from trac.tests.env import EnvironmentTestCase
@@ -164,11 +166,14 @@ class MultiproductTestCase(unittest.Test
self.env = env = EnvironmentStub(**kwargs)
if create_folder:
if path is None:
- env.path = tempfile.mkdtemp(prefix='bh-product-tempenv-')
+ env.path = tempfile.mkdtemp(prefix='bh-tempenv-')
else:
env.path = path
- if not os.path.exists(path):
- os.mkdir(path)
+ if not os.path.exists(env.path):
+ os.mkdir(env.path)
+ conf_dir = os.path.join(env.path, 'conf')
+ if not os.path.exists(conf_dir):
+ os.mkdir(conf_dir)
return env
def _setup_test_log(self, env):
@@ -223,8 +228,8 @@ class MultiproductTestCase(unittest.Test
r"""Apply multi product upgrades
"""
# Do not break wiki parser ( see #373 )
- env.disable_component(TicketModule)
- env.disable_component(ReportModule)
+ EnvironmentStub.disable_component_in_config(env, TicketModule)
+ EnvironmentStub.disable_component_in_config(env, ReportModule)
mpsystem = MultiProductSystem(env)
try:
@@ -395,6 +400,7 @@ class ProductEnvApiTestCase(Multiproduct
pass
# Let's pretend this was declared elsewhere
C.__module__ = 'dummy_module'
+ sys.modules['dummy_module'] = sys.modules[__name__]
global_env = self.env
product_env = self.product_env
@@ -411,6 +417,8 @@ class ProductEnvApiTestCase(Multiproduct
expected_rules = {
'multiproduct': True,
'trac': True,
+ 'trac.ticket.report.reportmodule': False,
+ 'trac.ticket.web_ui.ticketmodule': False,
'trac.db': True,
cname: False,
}
@@ -560,8 +568,7 @@ class ProductEnvHrefTestCase(Multiproduc
return decorator
def setUp(self):
- self._mp_setup()
- self.env.path = '/path/to/env'
+ self._mp_setup(create_folder=True)
self.env.abs_href = Href('http://globalenv.com/trac.cgi')
url_pattern = getattr(getattr(self, self._testMethodName).im_func,
'product_base_url', '')
@@ -570,6 +577,7 @@ class ProductEnvHrefTestCase(Multiproduc
self.product_env = ProductEnvironment(self.env, self.default_product)
def tearDown(self):
+ shutil.rmtree(os.path.dirname(self.env.path), ignore_errors=True)
# Release reference to transient environment mock object
if self.env is not None:
try:
@@ -675,11 +683,139 @@ class ProductEnvHrefTestCase(Multiproduc
product_base_url = staticmethod(product_base_url)
+class ProductEnvConfigTestCase(MultiproductTestCase):
+ """Test cases for product environment's configuration
+ """
+
+ class DummyAdminCommand(Component):
+ """Dummy class used for testing purposes
+ """
+ implements(IAdminCommandProvider)
+
+ class DummyException(Exception):
+ pass
+
+ def do_fail(self, *args):
+ raise DummyException(args)
+
+ def get_admin_commands(self):
+ yield "fail", "[ARG]...", "Always fail", None, self.do_fail
+
+
+ def setUp(self):
+ self._mp_setup(create_folder=True)
+ self.global_env = self.env
+ self.env = ProductEnvironment(self.global_env, self.default_product)
+
+ # Random component class
+ self.component_class = self.DummyAdminCommand
+
+ def tearDown(self):
+ if self.global_env is not None:
+ try:
+ self.global_env.reset_db()
+ except self.global_env.db_exc.OperationalError:
+ # "Database not found ...",
+ # "OperationalError: no such table: system" or the like
+ pass
+
+ shutil.rmtree(self.env.path)
+ self.env = self.global_env = None
+
+ def test_regression_bh_539(self):
+ tracadmin = AdminCommandManager(self.env)
+
+ self.assertTrue(self.env[self.component_class] is None,
+ "Expected component disabled")
+ self.assertFalse(any(isinstance(c, self.component_class)
+ for c in tracadmin.providers),
+ "Component erroneously listed in admin cmd providers")
+ self.assertEqual([], tracadmin.get_command_help(args=['fail']))
+
+ # Enable component in both global and product context
+ cmd_args = ['config', 'set', 'components', __name__ + '.*', 'enabled']
+ AdminCommandManager(self.global_env).execute_command(*cmd_args)
+ tracadmin.execute_command(*cmd_args)
+
+ self.assertTrue(self.env[self.component_class] is not None,
+ "Expected component enabled")
+ self.assertTrue(any(isinstance(c, self.component_class)
+ for c in tracadmin.providers),
+ "Component not listed in admin cmd providers")
+ self.assertEqual(1, len(tracadmin.get_command_help(args=['fail'])))
+
+ def test_regression_bh_539_concurrent(self):
+ try:
+ # It is necessary to load another environment object to work around
+ # ProductEnvironment class' parametric singleton constraint
+ old_env = self.env
+ # In-memory DB has to be shared
+ self.global_env.__class__.global_databasemanager = \
+ self.env.global_databasemanager
+ new_global_env = self._setup_test_env(create_folder=True,
+ path=self.global_env.path)
+ self.env = old_env
+ self._setup_test_log(new_global_env)
+
+ # FIXME: EnvironmentStub config is not bound to a real file
+ # ... so let's reuse one config for both envs to simulate that they
+ # are in sync, a condition verified in another test case
+ new_global_env.config = self.global_env.config
+
+ new_env = ProductEnvironment(new_global_env, self.default_product)
+
+ self.assertTrue(new_global_env is not self.global_env)
+ self.assertTrue(new_env is not self.env)
+ self.assertEqual(self.env.path, new_env.path)
+ self.assertEqual(self.env.config._lock_path,
+ new_env.config._lock_path)
+
+ tracadmin = AdminCommandManager(self.env)
+ new_tracadmin = AdminCommandManager(new_env)
+
+ # Assertions for self.env
+ self.assertTrue(self.env[self.component_class] is None,
+ "Expected component disabled")
+ self.assertFalse(any(isinstance(c, self.component_class)
+ for c in tracadmin.providers),
+ "Component erroneously listed in admin cmd "
+ "providers")
+ self.assertEqual([], tracadmin.get_command_help(args=['fail']))
+
+ # Repeat assertions for new_env
+ self.assertTrue(new_env[self.component_class] is None,
+ "Expected component disabled")
+ self.assertFalse(any(isinstance(c, self.component_class)
+ for c in new_tracadmin.providers),
+ "Component erroneously listed in admin cmd "
+ "providers")
+ self.assertEqual([], new_tracadmin.get_command_help(args=['fail']))
+
+ # Enable component in both self.global_env and self.env contexts
+ cmd_args = ['config', 'set', 'components',
+ __name__ + '.*', 'enabled']
+ AdminCommandManager(self.global_env).execute_command(*cmd_args)
+ tracadmin.execute_command(*cmd_args)
+
+ # Assert that changes are auto-magically reflected in new_env
+ self.assertTrue(new_env[self.component_class] is not None,
+ "Expected component enabled")
+ self.assertTrue(any(isinstance(c, self.component_class)
+ for c in new_tracadmin.providers),
+ "Component not listed in admin cmd providers")
+ self.assertEqual(
+ 1, len(new_tracadmin.get_command_help(args=['fail'])))
+ finally:
+ self.global_env.__class__.global_databasemanager = None
+ new_global_env = new_env = None
+
+
def test_suite():
return unittest.TestSuite([
unittest.makeSuite(ProductEnvTestCase, 'test'),
unittest.makeSuite(ProductEnvApiTestCase, 'test'),
unittest.makeSuite(ProductEnvHrefTestCase, 'test'),
+ unittest.makeSuite(ProductEnvConfigTestCase, 'test'),
])
if __name__ == '__main__':