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/02/27 03:14:38 UTC

svn commit: r1572404 [4/8] - in /bloodhound/branches/bep_0007_embeddable_objects: ./ bloodhound_dashboard/ bloodhound_dashboard/bhdashboard/ bloodhound_dashboard/bhdashboard/default-pages/ bloodhound_dashboard/bhdashboard/layouts/ bloodhound_dashboard/...

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -16,15 +17,17 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
+import logging
+import sys
 from collections import deque
 from fnmatch import fnmatch
-import sys
-try:
+if sys.version_info < (2, 7):
     import unittest2 as unittest
-except ImportError:
+else:
     import unittest
 
-from pkg_resources import resource_listdir, resource_isdir, resource_exists
+from pkg_resources import resource_exists, resource_filename, \
+                          resource_isdir, resource_listdir
 
 
 class TestLoader(unittest.TestLoader):
@@ -33,7 +36,8 @@ class TestLoader(unittest.TestLoader):
     sortTestMethodsUsing = cmp
     suiteClass = unittest.TestSuite
 
-    def discover_package(self, package_or_requirement, pattern='test*.py', ignore_subpkg_root=True):
+    def discover_package(self, package_or_requirement, pattern='*/test*.py',
+                         ignore_subpkg_root=True, exclude=None):
         """Find and return all test modules from the specified package
         directory, recursing into subdirectories to find them. Only test files
         that match the pattern will be loaded. (Using shell style pattern
@@ -43,17 +47,23 @@ class TestLoader(unittest.TestLoader):
         and registered with `pkg_resources` (e.g. via `setup.py develop`).
 
         If a target test module contains a '__testloader__' attribute then
-        related object will override current loader for every individual 
+        related object will override current loader for every individual
         module across the hierarchy.
         """
         pending = deque([(package_or_requirement, self, True)])
         tests = []
+        log = logging.getLogger('bh.tests')
+        if len(log.handlers) == 0:
+            # Configure logger instance. otherwise messages won't be displayed
+            _configure_logger(log)
         while pending:
             mdlnm, loader, isdir = pending.popleft()
             try:
                 mdl = self._get_module_from_name(mdlnm)
             except (ImportError, ValueError):
-                # Skip packages not having __init__.py
+                # Log import error and skip packages that don't import
+                log.exception('Discovered package %s but import failed',
+                              mdlnm)
                 continue
             loader = getattr(mdl, self.testLoaderAttribute, None) or loader
             if not (isdir and ignore_subpkg_root):
@@ -63,12 +73,18 @@ class TestLoader(unittest.TestLoader):
                     tests.append(loader.loadTestsFromModule(mdl))
             if isdir and resource_exists(mdlnm, '__init__.py'):
                 for fnm in resource_listdir(mdlnm, ''):
-                    if resource_isdir(mdlnm, fnm):
-                        pending.append( (mdlnm + '.' + fnm, loader, True) )
+                    fpath = resource_filename(mdlnm, fnm)
+                    if resource_isdir(mdlnm, fnm) \
+                            and (exclude is None
+                                 or not fnmatch(fpath + '/', exclude)):
+                        pending.append((mdlnm + '.' + fnm, loader, True))
                     elif any(fnm.endswith(ext) for ext in ['.py', '.pyc']) \
-                            and fnmatch(fnm, pattern) and fnm != '__init__.py':
+                            and fnmatch(fpath, pattern) \
+                            and fnm != '__init__.py'\
+                            and (exclude is None
+                                 or not fnmatch(fpath, exclude)):
                         submdlnm = mdlnm + '.' + fnm.rsplit('.', 1)[0]
-                        pending.append( (submdlnm, loader, False) )
+                        pending.append((submdlnm, loader, False))
         return self.suiteClass(tests)
 
     def _get_module_from_name(self, name):
@@ -76,8 +92,17 @@ class TestLoader(unittest.TestLoader):
         return sys.modules[name]
 
 
+def _configure_logger(log):
+    # See logging.basicConfig
+    handler = logging.StreamHandler()
+    formatter = logging.Formatter(logging.BASIC_FORMAT, None)
+    handler.setFormatter(formatter)
+    log.addHandler(handler)
+
+
 def test_suite():
-    return TestLoader().discover_package('tests', pattern='*.py')
+    return TestLoader().discover_package('tests', pattern='*.py',
+                                         exclude='*/functional/*')
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/__init__.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/__init__.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/__init__.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -22,4 +23,3 @@ from tests import TestLoader
 
 def test_suite():
     return TestLoader().discover_package(__name__, pattern='*.py')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console-tests.txt
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console-tests.txt?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console-tests.txt (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console-tests.txt Thu Feb 27 02:14:33 2014
@@ -1022,3 +1022,115 @@ name17  0     2010-01-18  val17  val17
 name18  0     2010-01-19  val18  val18
 name19  0     2010-01-20  val19  val19
 
+===== test_product_help_ok =====
+trac-admin - The Trac Administration Console 1.0.1
+help                 Show documentation
+initenv              Create and initialize a new environment
+attachment add       Attach a file to a resource
+attachment export    Export an attachment from a resource to a file or stdout
+attachment list      List attachments of a resource
+attachment remove    Remove an attachment from a resource
+changeset added      Notify trac about changesets added to a repository
+changeset modified   Notify trac about changesets modified in a repository
+component add        Add a new component
+component chown      Change component ownership
+component list       Show available components
+component remove     Remove/uninstall a component
+component rename     Rename a component
+config get           Get the value of the given option in "trac.ini"
+config remove        Remove the specified option from "trac.ini"
+config set           Set the value for the given option in "trac.ini"
+deploy               Extract static resources from Trac and all plugins
+hotcopy              Make a hot backup copy of an environment
+milestone add        Add milestone
+milestone completed  Set milestone complete date
+milestone due        Set milestone due date
+milestone list       Show milestones
+milestone remove     Remove milestone
+milestone rename     Rename milestone
+permission add       Add a new permission rule
+permission export    Export permission rules to a file or stdout as CSV
+permission import    Import permission rules from a file or stdin as CSV
+permission list      List permission rules
+permission remove    Remove a permission rule
+priority add         Add a priority value option
+priority change      Change a priority value
+priority list        Show possible ticket priorities
+priority order       Move a priority value up or down in the list
+priority remove      Remove a priority value
+repository add       Add a source repository
+repository alias     Create an alias for a repository
+repository list      List source repositories
+repository remove    Remove a source repository
+repository resync    Re-synchronize trac with repositories
+repository set       Set an attribute of a repository
+repository sync      Resume synchronization of repositories
+resolution add       Add a resolution value option
+resolution change    Change a resolution value
+resolution list      Show possible ticket resolutions
+resolution order     Move a resolution value up or down in the list
+resolution remove    Remove a resolution value
+session add          Create a session for the given sid
+session delete       Delete the session of the specified sid
+session list         List the name and email for the given sids
+session purge        Purge all anonymous sessions older than the given age
+session set          Set the name or email attribute of the given sid
+severity add         Add a severity value option
+severity change      Change a severity value
+severity list        Show possible ticket severities
+severity order       Move a severity value up or down in the list
+severity remove      Remove a severity value
+ticket remove        Remove ticket
+ticket_type add      Add a ticket type
+ticket_type change   Change a ticket type
+ticket_type list     Show possible ticket types
+ticket_type order    Move a ticket type up or down in the list
+ticket_type remove   Remove a ticket type
+upgrade              Upgrade database to current version
+version add          Add version
+version list         Show versions
+version remove       Remove version
+version rename       Rename version
+version time         Set version date
+wiki dump            Export wiki pages to files named by title
+wiki export          Export wiki page to file or stdout
+wiki import          Import wiki page from file or stdin
+wiki list            List wiki pages
+wiki load            Import wiki pages from files
+wiki remove          Remove wiki page
+wiki rename          Rename wiki page
+wiki replace         Replace the content of wiki pages from files (DANGEROUS!)
+wiki upgrade         Upgrade default wiki pages to current version
+===== test_product_help_version =====
+version add <name> [time]
+
+    Add version
+
+version list 
+
+    Show versions
+
+version remove <name>
+
+    Remove version
+
+version rename <name> <newname>
+
+    Rename version
+
+version time <name> <time>
+
+    Set version date
+
+===== test_product_help_version_add =====
+version add <name> [time]
+
+    Add version
+
+===== test_product_fail_version_add =====
+Error: Invalid arguments
+
+version add <name> [time]
+
+    Add version
+

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/console.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -20,16 +21,15 @@
 
 import os.path
 import sys
-from StringIO import StringIO
 import unittest
 
-from trac.admin.tests.console import load_expected_results, \
-        STRIP_TRAILING_SPACE, TracadminTestCase
+from trac.admin.tests.console import TracadminTestCase, load_expected_results
 
 from multiproduct.env import ProductEnvironment
 from tests.env import MultiproductTestCase
 
-class ProductTracadminTestCase(TracadminTestCase, MultiproductTestCase):
+
+class ProductTracAdminTestCase(TracadminTestCase, MultiproductTestCase):
 
     expected_results = load_expected_results(
             os.path.join(os.path.split(__file__)[0], 'console-tests.txt'),
@@ -40,14 +40,14 @@ class ProductTracadminTestCase(Tracadmin
         env = getattr(self, '_env', None)
         if env is None:
             self.global_env = self._setup_test_env(
-                    enable=('trac.*', 'multiproduct.*'), 
+                    enable=('trac.*', 'multiproduct.*'),
                     disable=('trac.tests.*',),
-                )
+            )
             self._upgrade_mp(self.global_env)
             self._setup_test_log(self.global_env)
             self._load_product_from_data(self.global_env, self.default_product)
-            self._env = env = ProductEnvironment(
-                    self.global_env, self.default_product)
+            self._env = env = ProductEnvironment(self.global_env,
+                                                 self.default_product)
             self._load_default_data(env)
         return env
 
@@ -59,41 +59,41 @@ class ProductTracadminTestCase(Tracadmin
         self.global_env.reset_db()
         self.global_env = self._env = None
 
-    def _execute(self, cmd, strip_trailing_space=True, input=None):
-        _in = sys.stdin
-        _err = sys.stderr
-        _out = sys.stdout
-        try:
-            if input:
-                sys.stdin = StringIO(input.encode('utf-8'))
-                sys.stdin.encoding = 'utf-8' # fake input encoding
-            sys.stderr = sys.stdout = out = StringIO()
-            out.encoding = 'utf-8' # fake output encoding
-            retval = None
-            try:
-                retval = self._admin.onecmd(cmd)
-            except SystemExit:
-                pass
-            value = out.getvalue()
-            if isinstance(value, str): # reverse what print_listing did
-                value = value.decode('utf-8')
-            if retval != 0:
-                self.env.log.debug('trac-admin failure: %s', value)
-            if strip_trailing_space:
-                return retval, STRIP_TRAILING_SPACE.sub('', value)
-            else:
-                return retval, value
-        finally:
-            sys.stdin = _in
-            sys.stderr = _err
-            sys.stdout = _out
+    def test_product_help_ok(self):
+        self._admin.env_set('', self.global_env)
+        from trac import __version__
+        test_name = sys._getframe().f_code.co_name
+        expected_results = self.expected_results[test_name] \
+                           % {'version': __version__}
+        rv, output = self._execute('product admin %s help'
+                                   % self.default_product)
+        self.assertEqual(0, rv)
+        self.assertEqual(expected_results, output)
+
+    def test_product_help_version(self):
+        rv, output = self._execute('help version')
+        self.assertEqual(0, rv)
+        expected = self.expected_results[self._testMethodName]
+        self.assertEqual(expected, output)
+
+    def test_product_help_version_add(self):
+        rv, output = self._execute('help version add')
+        self.assertEqual(0, rv)
+        expected = self.expected_results[self._testMethodName]
+        self.assertEqual(expected, output)
+
+    def test_product_fail_version_add(self):
+        rv, output = self._execute('version add v x y')
+        self.assertEqual(2, rv)
+        expected = self.expected_results[self._testMethodName]
+        self.assertEqual(expected, output)
 
 
 def test_suite():
     return unittest.TestSuite([
-            unittest.makeSuite(ProductTracadminTestCase,'test'),
-        ])
+            unittest.makeSuite(ProductTracAdminTestCase),
+    ])
+
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/product_admin.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/product_admin.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/product_admin.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/admin/product_admin.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -54,11 +55,11 @@ class TestAdminPanel(Component):
             yield 'testcat1', 'Test category 1', 'panel1', 'Test panel 1'
             yield 'testcat1', 'Test category 1', 'panel2', 'Test panel 2'
             yield 'testcat1', 'Test category 1', 'panel3', 'Test panel 3'
-    
+
             yield 'testcat2', 'Test category 2', 'panel1', 'Test panel 1'
             yield 'testcat2', 'Test category 2', 'panel_2', 'Test panel 2'
             yield 'testcat2', 'Test category 2', 'panel-3', 'Test panel 3'
-    
+
             yield 'testcat3', 'Test category 3', 'panel1', 'Test panel 1'
             yield 'testcat3', 'Test category 3', 'panel2', 'Test panel 2'
 
@@ -106,10 +107,10 @@ class BaseProductAdminPanelTestCase(Mult
     def setUp(self):
         self._mp_setup(enable=[AdminModule, DefaultPermissionPolicy,
                                DefaultPermissionStore, PermissionSystem,
-                               PluginAdminPanel, RequestDispatcher, 
+                               PluginAdminPanel, RequestDispatcher,
                                api.MultiProductSystem,
                                product_admin.ProductAdminModule,
-                               PanelsWhitelist, SectionWhitelist, 
+                               PanelsWhitelist, SectionWhitelist,
                                TestAdminPanel, TestPermissionRequestor])
         self.global_env = self.env
         self.env = ProductEnvironment(self.global_env, self.default_product)
@@ -125,10 +126,10 @@ class BaseProductAdminPanelTestCase(Mult
 
 
 class ProductAdminSetupTestCase(BaseProductAdminPanelTestCase):
-    ALL_PANELS = [('testcat1', 'panel1'), ('testcat1', 'panel2'), 
-                  ('testcat1', 'panel3'), ('testcat2', 'panel_1'), 
-                  ('testcat2', 'panel-2'), ('testcat2', 'panel3'), 
-                  ('testcat3', 'panel1'), ('testcat3', 'panel2'), 
+    ALL_PANELS = [('testcat1', 'panel1'), ('testcat1', 'panel2'),
+                  ('testcat1', 'panel3'), ('testcat2', 'panel_1'),
+                  ('testcat2', 'panel-2'), ('testcat2', 'panel3'),
+                  ('testcat3', 'panel1'), ('testcat3', 'panel2'),
                   ('general', 'plugin'), ]
 
     def test_init_whitelist(self):
@@ -137,7 +138,7 @@ class ProductAdminSetupTestCase(BaseProd
                           ('testcat1', 'panel1') : True,
                           ('testcat1', 'panel3'): True,
                           ('testcat2', 'panel3'): True,
-                          ('general', 'plugin') : True,}, 
+                          ('general', 'plugin') : True,},
                          self.product_admin.acl)
         self.assertTrue(all(not self.global_product_admin._check_panel(c, p)
                             for c, p in self.ALL_PANELS))
@@ -153,9 +154,9 @@ class ProductAdminSetupTestCase(BaseProd
         self.assertFalse(self.product_admin._check_panel('other', 'panel'))
 
     def test_init_blacklist(self):
-        self.global_env.config.set('multiproduct', 'admin_blacklist', 
+        self.global_env.config.set('multiproduct', 'admin_blacklist',
                                    'testcat1:panel1,testcat3:panel2')
-        self.env.config.set('multiproduct', 'admin_blacklist', 
+        self.env.config.set('multiproduct', 'admin_blacklist',
                             'testcat1:panel3,testcat3:panel1,testcat2:*')
 
         self.assertEqual(['testcat1:panel1','testcat3:panel2'],
@@ -170,7 +171,7 @@ class ProductAdminSetupTestCase(BaseProd
                           ('testcat1', 'panel3'): False,
                           ('testcat2', 'panel3'): True,
                           ('testcat3', 'panel1'): False,
-                          ('general', 'plugin'): True,}, 
+                          ('general', 'plugin'): True,},
                          self.product_admin.acl)
 
         self.assertTrue(all(not self.global_product_admin._check_panel(c, p)
@@ -192,9 +193,9 @@ class ProductAdminDispatchTestCase(BaseP
 
     def setUp(self):
         BaseProductAdminPanelTestCase.setUp(self)
-        self.global_env.config.set('multiproduct', 'admin_blacklist', 
+        self.global_env.config.set('multiproduct', 'admin_blacklist',
                                    'testcat1:panel1,testcat3:panel2')
-        self.env.config.set('multiproduct', 'admin_blacklist', 
+        self.env.config.set('multiproduct', 'admin_blacklist',
                             'testcat1:panel3,testcat3:panel1,testcat2:*')
         global_permsys = PermissionSystem(self.global_env)
         permsys = PermissionSystem(self.env)
@@ -502,4 +503,3 @@ def test_suite():
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/attachment.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/attachment.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/attachment.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/attachment.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -68,4 +69,3 @@ def test_suite():
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/config.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/config.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/config.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/config.py Thu Feb 27 02:14:33 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)
@@ -82,13 +82,13 @@ class ProductConfigTestCase(Configuratio
         parser.readfp(fp, 'bh-product-test')
         with self.env.db_transaction as db:
             # Delete existing setting for target product , if any
-            for setting in ProductSetting.select(self.env, db, 
+            for setting in ProductSetting.select(self.env, db,
                     {'product' : product}):
                 setting.delete()
             # Insert new options
             for section in parser.sections():
                 option_key = dict(
-                        section=to_unicode(section), 
+                        section=to_unicode(section),
                         product=to_unicode(product)
                     )
                 for option, value in parser.items(section):
@@ -121,7 +121,7 @@ class ProductConfigTestCase(Configuratio
     def _dump_settings(self, config):
         product = config.product
         fields = ('section', 'option', 'value')
-        rows = [tuple(getattr(s, f, None) for f in fields) for s in 
+        rows = [tuple(getattr(s, f, None) for f in fields) for s in
                 ProductSetting.select(config.env, where={'product' : product})]
 
         dump = []
@@ -131,7 +131,12 @@ class ProductConfigTestCase(Configuratio
                 dump.append('%s = %s\n' % (row[1], row[2]))
         return dump
 
-    # Test cases rewritten to avoid reading config file. 
+
+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
 
     def test_set_and_save(self):
@@ -150,12 +155,12 @@ class ProductConfigTestCase(Configuratio
         dump = self._dump_settings(config)
         self.assertEquals([
                            u'[aä]\n',
-                           u"option1 = Voilà l'été\n", 
-                           u"option2 = Voilà l'été\n", 
-                           u'öption0 = x\n', 
-                           # u"option3 = Voilà l'été\n", 
+                           u"option1 = Voilà l'été\n",
+                           u"option2 = Voilà l'été\n",
+                           u'öption0 = x\n',
+                           # u"option3 = Voilà l'été\n",
                            u'[b]\n',
-                           u'öption0 = y\n', 
+                           u'öption0 = y\n',
                            ],
                           dump)
         config2 = self._read()
@@ -177,10 +182,10 @@ class ProductConfigTestCase(Configuratio
             dump = self._dump_settings(config)
             self.assertEquals([
                                u'[a]\n',
-                               u"option1 = Voilà l'été\n", 
-                               u"option2 = Voilà l'été\n", 
+                               u"option1 = Voilà l'été\n",
+                               u"option2 = Voilà l'été\n",
                                u'[inherit]\n',
-                               u"file = trac-site.ini\n", 
+                               u"file = trac-site.ini\n",
                                ],
                               dump)
             config2 = self._read()
@@ -196,9 +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/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/core.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/core.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/core.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/core.py Thu Feb 27 02:14:33 2014
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -15,16 +17,13 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-import sys
-if sys.version < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
+from tests import unittest
 
 from trac.core import Interface, implements, Component
 
 from multiproduct.core import MultiProductExtensionPoint
 
+
 class MultiProductExtensionPointTestCase(unittest.TestCase):
     def setUp(self):
         from trac.core import ComponentManager, ComponentMeta

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/__init__.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/__init__.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/__init__.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -20,4 +21,3 @@ from tests import TestLoader
 
 def test_suite():
     return TestLoader().discover_package(__name__, pattern='*.py')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/api.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/api.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/api.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/api.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -1057,5 +1058,3 @@ class DbCursorTestCase(unittest.TestCase
 
 if __name__ == '__main__':
     unittest.main()
-
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/mysql.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/mysql.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/mysql.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/mysql.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/postgres.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/postgres.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/postgres.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/postgres.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/util.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/util.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/util.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/util.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py Thu Feb 27 02:14:33 2014
@@ -19,23 +19,17 @@
 
 """Tests for Apache(TM) Bloodhound's product environments"""
 
-from inspect import stack
 import os.path
 import shutil
-from sqlite3 import OperationalError
 import sys
 import tempfile
+from inspect import stack
+from tests import unittest
 from types import MethodType
 
-if sys.version_info < (2, 7):
-    import unittest2 as unittest
-    from unittest2.case import _AssertRaisesContext
-else:
-    import unittest
-    from unittest.case import _AssertRaisesContext
-
+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
@@ -44,10 +38,12 @@ from trac.ticket.web_ui import TicketMod
 from trac.util.text import to_unicode
 from trac.web.href import Href
 
-from multiproduct.api import MultiProductSystem
+from multiproduct.api import DB_VERSION, MultiProductSystem
 from multiproduct.env import ProductEnvironment
 from multiproduct.model import Product
 
+_AssertRaisesContext = unittest.case._AssertRaisesContext
+
 
 class ProductEnvironmentStub(ProductEnvironment):
     r"""A product environment slightly tweaked for testing purposes
@@ -93,7 +89,7 @@ class MultiproductTestCase(unittest.Test
                 return _AssertRaisesContext.__exit__(self, exc_type,
                                                      exc_value, tb)
             except self.failureException, exc:
-                msg = self.test_case.exceptFailureMessage 
+                msg = self.test_case.exceptFailureMessage
                 if msg is not None:
                     standardMsg = str(exc)
                     msg = msg % self._tb_locals(tb)
@@ -160,7 +156,7 @@ class MultiproductTestCase(unittest.Test
     # Test setup
 
     def _setup_test_env(self, create_folder=True, path=None, **kwargs):
-        r"""Prepare a new test environment . 
+        r"""Prepare a new test environment .
 
         Optionally set its path to a meaningful location (temp folder
         if `path` is `None`).
@@ -170,20 +166,27 @@ class MultiproductTestCase(unittest.Test
         self.env = env = EnvironmentStub(**kwargs)
         if create_folder:
             if path is None:
-                env.path = tempfile.mkdtemp('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):
         r"""Ensure test product with prefix is loaded
         """
-        logdir = tempfile.gettempdir()
-        logpath = os.path.join(logdir, 'trac-testing.log')
+        if not hasattr(env, 'path') or not env.path:
+            env.path = tempfile.mkdtemp(prefix='bh-product-tempenv-')
+        log_dir = os.path.join(env.path, 'log')
+        if not os.path.exists(log_dir):
+            os.mkdir(log_dir)
+        log_file = os.path.join(log_dir, 'trac-testing.log')
         config = env.config
-        config.set('logging', 'log_file', logpath)
+        config.set('logging', 'log_file', log_file)
         config.set('logging', 'log_type', 'file')
         config.set('logging', 'log_level', 'DEBUG')
 
@@ -196,7 +199,7 @@ class MultiproductTestCase(unittest.Test
 
         # Clean-up logger instance and associated handler
         # Otherwise large test suites will only result in ERROR eventually
-        # (at least in Unix systems) with messages 
+        # (at least in Unix systems) with messages
         #
         # TracError: Error reading '/path/to/file', make sure it is readable.
         # error: /path/to/: Too many open files
@@ -225,16 +228,20 @@ 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:
-            mpsystem.upgrade_environment(env.db_transaction)
-        except OperationalError:
-            # Database is upgraded, but database version was deleted.
-            # Complete the upgrade by inserting default product.
-            mpsystem._insert_default_product(env.db_transaction)
+        with env.db_transaction as db:
+            try:
+                mpsystem.upgrade_environment(db)
+            except env.db_exc.OperationalError:
+                # Database is upgraded, but database version was deleted.
+                # Complete the upgrade by inserting default product.
+                mpsystem._insert_default_product(db)
+            finally:
+                # Ensure that multiproduct DB version is set to latest value
+                mpsystem._update_db_version(db, DB_VERSION)
         # assume that the database schema has been upgraded, enable
         # multi-product schema support in environment
         env.enable_multiproduct_schema(True)
@@ -311,7 +318,7 @@ class ProductEnvApiTestCase(Multiproduct
         if self.env is not None:
             try:
                 self.env.reset_db()
-            except OperationalError:
+            except self.env.db_exc.OperationalError:
                 # "Database not found ...",
                 # "OperationalError: no such table: system" or the like
                 pass
@@ -333,7 +340,7 @@ class ProductEnvApiTestCase(Multiproduct
 
         def property_mock(attrnm, expected_self):
             def assertAttrFwd(instance):
-                self.assertIs(instance, expected_self, 
+                self.assertIs(instance, expected_self,
                               "Mismatch in property '%s'" % (attrnm,))
                 raise AttrSuccess
             return property(assertAttrFwd)
@@ -343,7 +350,7 @@ class ProductEnvApiTestCase(Multiproduct
             for attrnm in 'system_info_providers secure_cookies ' \
                     'project_admin_trac_url get_system_info get_version ' \
                     'get_templates_dir get_templates_dir get_log_dir ' \
-                    'backup'.split(): 
+                    'backup'.split():
                 original = getattr(Environment, attrnm)
                 if isinstance(original, MethodType):
                     translation = getattr(self.product_env, attrnm)
@@ -388,15 +395,8 @@ class ProductEnvApiTestCase(Multiproduct
     def test_typecheck(self):
         """Testing env.__init__"""
         self._load_product_from_data(self.env, 'tp2')
-        with self.assertRaises(TypeError) as cm_test:
-            new_env = ProductEnvironment(self.product_env, 'tp2')
-
-        msg = str(cm_test.exception)
-        expected_msg = "Initializer must be called with " \
-                       "trac.env.Environment instance as first argument " \
-                       "(got multiproduct.env.ProductEnvironment instance " \
-                       "instead)"
-        self.assertEqual(msg, expected_msg)
+        env2 = ProductEnvironment(self.product_env, 'tp2')
+        self.assertIs(env2, ProductEnvironment(self.env, 'tp2'))
 
     def test_component_enable(self):
         """Testing env.is_component_enabled"""
@@ -404,6 +404,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
@@ -420,6 +421,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,
             }
@@ -520,24 +523,24 @@ class ProductEnvApiTestCase(Multiproduct
         self.assertEquals('value2', product_config['section'].get('key'))
 
     def test_parametric_singleton(self):
-        self.assertIs(self.product_env, 
+        self.assertIs(self.product_env,
                       ProductEnvironment(self.env, self.default_product))
 
         for prefix in self.PRODUCT_DATA:
             if prefix != self.default_product:
                 self._load_product_from_data(self.env, prefix)
 
-        envgen1 = dict([prefix, ProductEnvironment(self.env, prefix)] 
+        envgen1 = dict([prefix, ProductEnvironment(self.env, prefix)]
                        for prefix in self.PRODUCT_DATA)
-        envgen2 = dict([prefix, ProductEnvironment(self.env, prefix)] 
+        envgen2 = dict([prefix, ProductEnvironment(self.env, prefix)]
                        for prefix in self.PRODUCT_DATA)
 
         for prefix, env1 in envgen1.iteritems():
-            self.assertIs(env1, envgen2[prefix], 
+            self.assertIs(env1, envgen2[prefix],
                           "Identity check (by prefix) '%s'" % (prefix,))
 
         for prefix, env1 in envgen1.iteritems():
-            self.assertIs(env1, envgen2[prefix], 
+            self.assertIs(env1, envgen2[prefix],
                           "Identity check (by prefix) '%s'" % (prefix,))
 
         def load_product(prefix):
@@ -552,12 +555,12 @@ class ProductEnvApiTestCase(Multiproduct
                        for prefix in self.PRODUCT_DATA)
 
         for prefix, env1 in envgen1.iteritems():
-            self.assertIs(env1, envgen3[prefix], 
+            self.assertIs(env1, envgen3[prefix],
                           "Identity check (by product model) '%s'" % (prefix,))
 
 
 class ProductEnvHrefTestCase(MultiproductTestCase):
-    """Assertions for resolution of product environment's base URL 
+    """Assertions for resolution of product environment's base URL
     [https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003 BEP 3]
     """
 
@@ -570,7 +573,6 @@ class ProductEnvHrefTestCase(Multiproduc
 
     def setUp(self):
         self._mp_setup()
-        self.env.path = '/path/to/env'
         self.env.abs_href = Href('http://globalenv.com/trac.cgi')
         url_pattern = getattr(getattr(self, self._testMethodName).im_func,
                               'product_base_url', '')
@@ -579,11 +581,12 @@ 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:
                 self.env.reset_db()
-            except OperationalError:
+            except self.env.db_exc.OperationalError:
                 # "Database not found ...",
                 # "OperationalError: no such table: system" or the like
                 pass
@@ -610,8 +613,11 @@ class ProductEnvHrefTestCase(Multiproduc
     def test_href_inherit_sibling_paths(self):
         """Test product base URL at sibling paths inheriting configuration.
         """
-        self.assertEqual('/trac.cgi/env/tp1', self.product_env.href())
-        self.assertEqual('http://globalenv.com/trac.cgi/env/tp1',
+        self.assertEqual('/trac.cgi/%s/tp1'
+                         % os.path.split(self.env.path)[-1],
+                         self.product_env.href())
+        self.assertEqual('http://globalenv.com/trac.cgi/%s/tp1'
+                         % os.path.split(self.env.path)[-1],
                          self.product_env.abs_href())
 
     @product_base_url('')
@@ -635,7 +641,9 @@ class ProductEnvHrefTestCase(Multiproduc
         """Test complex product base URL
         """
         self.assertEqual('/bh/tp1', self.product_env.href())
-        self.assertEqual('http://env.tld/bh/tp1', self.product_env.abs_href())
+        self.assertEqual('http://%s.tld/bh/tp1'
+                         % os.path.split(self.env.path)[-1],
+                         self.product_env.abs_href())
 
     @product_base_url('http://$(prefix)s.$(envname)s.tld/')
     def test_product_href_uses_multiproduct_product_base_url(self):
@@ -651,7 +659,9 @@ class ProductEnvHrefTestCase(Multiproduc
         # Product URLs
         self.assertEqual('', self.product_env.base_url)
         self.assertEqual('/', self.product_env.href())
-        self.assertEqual('http://tp1.env.tld', self.product_env.abs_href())
+        self.assertEqual('http://tp1.%s.tld'
+                         % os.path.split(self.env.path)[-1],
+                         self.product_env.abs_href())
 
     @product_base_url('http://$(prefix)s.$(envname)s.tld/')
     def test_product_href_uses_products_base_url(self):
@@ -679,16 +689,146 @@ class ProductEnvHrefTestCase(Multiproduc
 
         self.assertEqual('', self.product_env.base_url)
         self.assertEqual('/', self.product_env.href())
-        self.assertEqual('http://tp1.env.tld', self.product_env.abs_href())
+        self.assertEqual('http://tp1.%s.tld'
+                         % os.path.split(self.env.path)[-1],
+                         self.product_env.abs_href())
 
     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__':

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/hooks.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/hooks.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/hooks.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/hooks.py Thu Feb 27 02:14:33 2014
@@ -17,11 +17,7 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
+from tests import unittest
 
 from trac.web.href import Href
 

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/__init__.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/__init__.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/__init__.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -22,4 +23,3 @@ from tests import TestLoader
 
 def test_suite():
     return TestLoader().discover_package(__name__, pattern='*.py')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/api.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/api.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/api.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/api.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -60,4 +61,3 @@ def test_suite():
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/patch.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/patch.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/patch.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/patch.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -64,4 +65,3 @@ def test_suite():
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/pygments.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/pygments.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/pygments.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/mimeview/pygments.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -18,19 +19,15 @@
 
 """Tests for Apache(TM) Bloodhound's Pygments renderer in product environments"""
 
-import sys
-if sys.version_info < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
+from tests import unittest
 
+from tests.env import MultiproductTestCase
 from trac.mimeview.api import Mimeview
 from trac.mimeview.pygments import PygmentsRenderer
-from trac.mimeview.tests import pygments as test_pygments 
+from trac.mimeview.tests import pygments as test_pygments
 from trac.web.chrome import Chrome
 
 from multiproduct.env import ProductEnvironment
-from tests.env import MultiproductTestCase
 
 have_pygments = False
 
@@ -42,6 +39,7 @@ else:
         test_python_hello = test_python_hello_mimeview = \
                 lambda self : None
 
+
 class ProductPygmentsRendererTestCase(super_class, MultiproductTestCase):
 
     @property
@@ -71,7 +69,7 @@ class ProductPygmentsRendererTestCase(su
         self.global_env = self._env = None
 
 ProductPygmentsRendererTestCase = unittest.skipUnless(
-        test_pygments.have_pygments, 
+        test_pygments.have_pygments,
         'mimeview/tests/pygments (no pygments installed)'
     )(ProductPygmentsRendererTestCase)
 
@@ -82,4 +80,3 @@ def test_suite():
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py Thu Feb 27 02:14:33 2014
@@ -1,4 +1,5 @@
-
+# -*- coding: utf-8 -*-
+#
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -18,26 +19,18 @@
 
 """Tests for multiproduct/model.py"""
 import shutil
-import sys
 import tempfile
+from tests import unittest
 
-if sys.version_info < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
-
-from sqlite3 import OperationalError
-
-from trac.test import EnvironmentStub
 from trac.core import TracError
+from trac.test import EnvironmentStub
+from trac.tests.resource import TestResourceChangeListener
 from trac.ticket.model import Ticket
 
-from multiproduct.env import ProductEnvironment
-from multiproduct.model import Product
 from bhdashboard.model import ModelBase
-
 from multiproduct.api import MultiProductSystem
-from trac.tests.resource import TestResourceChangeListener
+from multiproduct.env import ProductEnvironment
+from multiproduct.model import Product
 
 
 class ProductTestCase(unittest.TestCase):
@@ -48,15 +41,15 @@ class ProductTestCase(unittest.TestCase)
 
     def setUp(self):
         self.env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
-        self.env.path = tempfile.mkdtemp('bh-product-tempenv')
-        
+        self.env.path = tempfile.mkdtemp(prefix='bh-product-tempenv-')
+
         self.mpsystem = MultiProductSystem(self.env)
         try:
             self.mpsystem.upgrade_environment(self.env.db_transaction)
-        except OperationalError:
+        except self.env.db_exc.OperationalError:
             # table remains but database version is deleted
             pass
-        
+
         self.listener = self._enable_resource_change_listener()
         self.default_data = {'prefix':self.INITIAL_PREFIX,
                              'name':self.INITIAL_NAME,
@@ -70,7 +63,7 @@ class ProductTestCase(unittest.TestCase)
     def tearDown(self):
         shutil.rmtree(self.env.path)
         self.env.reset_db()
-    
+
     def _enable_resource_change_listener(self):
         listener = TestResourceChangeListener(self.env)
         listener.resource_type = Product
@@ -89,36 +82,36 @@ class ProductTestCase(unittest.TestCase)
         test = {'prefix': 'td',
                 'name': 'test field access',
                 'description': 'product to test field setting'}
-        
+
         product = Product(self.env)
-        
+
         # attempt to set the fields from the data
         product.prefix = test['prefix']
         product.name = test['name']
         product.description = test['description']
-        
+
         self.assertEqual(product._data['prefix'], test['prefix'])
         self.assertEqual(product._data['name'], test['name'])
         self.assertEqual(product._data['description'], test['description'])
-    
+
     def test_select(self):
         """tests that select can search Products by fields"""
-        
+
         p2_data = {'prefix':'tp2',
                    'name':'test project 2',
                    'description':'a different test project'}
         p3_data = {'prefix':'tp3',
                    'name':'test project 3',
                    'description':'test project'}
-        
+
         product2 = Product(self.env)
         product2._data.update(p2_data)
         product3 = Product(self.env)
         product3._data.update(p3_data)
-        
+
         product2.insert()
         product3.insert()
-        
+
         products = list(Product.select(self.env, where={'prefix':'tp'}))
         self.assertEqual(1, len(products))
         products = list(Product.select(self.env,
@@ -127,43 +120,43 @@ class ProductTestCase(unittest.TestCase)
         products = list(Product.select(self.env,
             where={'prefix':'tp3', 'name':'test project 3'}))
         self.assertEqual(1, len(products))
-    
+
     def test_update(self):
         """tests that we can use update to push data to the database"""
         product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
         self.assertEqual('test project', product._data['name'])
-        
-        new_data = {'prefix':'tp', 
-                    'name':'updated', 
+
+        new_data = {'prefix':'tp',
+                    'name':'updated',
                     'description':'nothing'}
         product._data.update(new_data)
         product.update()
-        
+
         comp_product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
         self.assertEqual('updated', comp_product._data['name'])
-    
+
     def test_update_key_change(self):
         """tests that we raise an error for attempting to update key fields"""
-        bad_data = {'prefix':'tp0', 
-                    'name':'update', 
+        bad_data = {'prefix':'tp0',
+                    'name':'update',
                     'description':'nothing'}
         product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
         product._data.update(bad_data)
         self.assertRaises(TracError, product.update)
-    
+
     def test_insert(self):
         """test saving new Product"""
         data = {'prefix':'new', 'name':'new', 'description':'new'}
         product = Product(self.env)
         product._data.update(data)
         product.insert()
-        
+
         check_products = list(Product.select(self.env, where={'prefix':'new'}))
-        
+
         self.assertEqual(product._data['prefix'],
                          check_products[0]._data['prefix'])
         self.assertEqual(1, len(check_products))
-    
+
     def test_insert_duplicate_key(self):
         """test attempted saving of Product with existing key fails"""
         dupe_key_data = {'prefix':'tp',
@@ -172,22 +165,22 @@ class ProductTestCase(unittest.TestCase)
         product2 = Product(self.env)
         product2._data.update(dupe_key_data)
         self.assertRaises(TracError, product2.insert)
-    
+
     def test_delete(self):
         """test that we are able to delete Products"""
         product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
         product.delete()
-        
+
         post = list(Product.select(self.env, where={'prefix':'tp'}))
         self.assertEqual(0, len(post))
-        
+
     def test_delete_twice(self):
         """test that we error when deleting twice on the same key"""
         product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
         product.delete()
-        
+
         self.assertRaises(TracError, product.delete)
-    
+
     def test_field_data_get(self):
         """tests that we can use table.field syntax to get to the field data"""
         prefix = self.default_data['prefix']
@@ -197,12 +190,12 @@ class ProductTestCase(unittest.TestCase)
         self.assertEqual(prefix, product.prefix)
         self.assertEqual(name, product.name)
         self.assertEqual(description, product.description)
-    
+
     def test_field_set(self):
         """tests that we can use table.field = something to set field data"""
         prefix = self.default_data['prefix']
         product = list(Product.select(self.env, where={'prefix':prefix}))[0]
-        
+
         new_description = 'test change of description'
         product.description = new_description
         self.assertEqual(new_description, product.description)
@@ -226,7 +219,7 @@ class ProductTestCase(unittest.TestCase)
             for table in schema:
                 for statement in db_connector.to_sql(table):
                     db(statement)
-        
+
         structure =  dict([(table.name, [col.name for col in table.columns])
                            for table in schema])
         tm1 = TestModel(self.env)
@@ -292,4 +285,3 @@ def suite():
 
 if __name__ == '__main__':
     unittest.main()
-

Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/perm.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/perm.py?rev=1572404&r1=1572403&r2=1572404&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/perm.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/perm.py Thu Feb 27 02:14:33 2014
@@ -40,7 +40,7 @@ from tests.env import MultiproductTestCa
 perm.DefaultPermissionPolicy.CACHE_EXPIRY = 0
 
 
-class ProductDefaultPermissionStoreTestCase(DefaultPermissionStoreTestCase, 
+class ProductDefaultPermissionStoreTestCase(DefaultPermissionStoreTestCase,
         MultiproductTestCase):
 
     def setUp(self):
@@ -64,17 +64,17 @@ class ProductDefaultPermissionStoreTestC
         store1 = perm.DefaultPermissionStore(env1)
 
         global_env.db_transaction.executemany(
-            "INSERT INTO permission VALUES (%s,%s)", 
+            "INSERT INTO permission VALUES (%s,%s)",
             [('dev', 'WIKI_MODIFY'),
              ('dev', 'REPORT_ADMIN'),
              ('john', 'dev')])
         env.db_transaction.executemany(
-            "INSERT INTO permission VALUES (%s,%s)", 
+            "INSERT INTO permission VALUES (%s,%s)",
             [('dev', 'WIKI_VIEW'),
              ('dev', 'REPORT_VIEW'),
              ('john', 'dev')])
         env1.db_transaction.executemany(
-            "INSERT INTO permission VALUES (%s,%s)", 
+            "INSERT INTO permission VALUES (%s,%s)",
             [('dev', 'TICKET_CREATE'),
              ('dev', 'MILESTONE_VIEW'),
              ('john', 'dev')])
@@ -185,7 +185,7 @@ class SudoTestCase(ProductPermissionCach
 
         with self.assertRaises(RuntimeError) as test_cm:
             sudoperm.has_permission('TEST_MODIFY')
-        self.assertEqual('Permission check out of context', 
+        self.assertEqual('Permission check out of context',
                          str(test_cm.exception))
 
         with self.assertRaises(ValueError) as test_cm:
@@ -210,30 +210,30 @@ class SudoTestCase(ProductPermissionCach
 
         sudoperm = sudo(None, 'TEST_CREATE', ['TRAC_ADMIN'])
         sudoperm.perm = self.perm
-        
+
         self.assertTrue(sudoperm.has_permission('EMAIL_VIEW'))
 
     def test_sudo_ambiguous(self):
         with self.assertRaises(ValueError) as test_cm:
-            sudo(None, 'TEST_MODIFY', ['TEST_MODIFY', 'TEST_DELETE'], 
+            sudo(None, 'TEST_MODIFY', ['TEST_MODIFY', 'TEST_DELETE'],
                  ['TEST_MODIFY', 'TEST_CREATE'])
-        self.assertEquals('Impossible to grant and revoke (TEST_MODIFY)', 
+        self.assertEquals('Impossible to grant and revoke (TEST_MODIFY)',
                           str(test_cm.exception))
 
         with self.assertRaises(ValueError) as test_cm:
-            sudoperm = sudo(None, 'TEST_MODIFY', ['TEST_ADMIN'], 
+            sudoperm = sudo(None, 'TEST_MODIFY', ['TEST_ADMIN'],
                  ['TEST_MODIFY', 'TEST_CREATE'])
             sudoperm.perm = self.perm
         self.assertEquals('Impossible to grant and revoke '
-                          '(TEST_CREATE, TEST_MODIFY)', 
+                          '(TEST_CREATE, TEST_MODIFY)',
                           str(test_cm.exception))
 
         with self.assertRaises(ValueError) as test_cm:
             req = Mock(perm=self.perm)
-            sudo(req, 'TEST_MODIFY', ['TEST_ADMIN'], 
+            sudo(req, 'TEST_MODIFY', ['TEST_ADMIN'],
                  ['TEST_MODIFY', 'TEST_CREATE'])
         self.assertEquals('Impossible to grant and revoke '
-                          '(TEST_CREATE, TEST_MODIFY)', 
+                          '(TEST_CREATE, TEST_MODIFY)',
                           str(test_cm.exception))
 
     # Sudo permission context equivalent to  permissions cache
@@ -254,9 +254,9 @@ class SudoTestCase(ProductPermissionCach
 
     for tcnm in tcnames:
         f1 = _test_with_sudo_rules(tcnm, '', [])
-        f2 = _test_with_sudo_rules(tcnm, 'test_sudo_partial_', 
+        f2 = _test_with_sudo_rules(tcnm, 'test_sudo_partial_',
                                    ['TEST_MODIFY'])
-        f3 = _test_with_sudo_rules(tcnm, 'test_sudo_full_', 
+        f3 = _test_with_sudo_rules(tcnm, 'test_sudo_full_',
                                    ['TEST_MODIFY', 'TEST_ADMIN'])
         for f in (f1, f2, f3):
             _gen_tests[f.func_name] = f
@@ -267,7 +267,7 @@ list(setattr(SudoTestCase, tcnm, f)
      for tcnm, f in SudoTestCase._gen_tests.iteritems())
 
 
-class ProductPermissionPolicyTestCase(PermissionPolicyTestCase, 
+class ProductPermissionPolicyTestCase(PermissionPolicyTestCase,
                                            MultiproductTestCase):
     @property
     def env(self):
@@ -294,7 +294,7 @@ class ProductPermissionPolicyTestCase(Pe
     def setUp(self):
         super(ProductPermissionPolicyTestCase, self).setUp()
 
-        self.global_env.config.set('trac', 'permission_policies', 
+        self.global_env.config.set('trac', 'permission_policies',
                                    'DefaultPermissionPolicy')
         self.permsys = perm.PermissionSystem(self.env)
         self.global_perm_admin = perm.PermissionAdmin(self.global_env)
@@ -309,7 +309,7 @@ class ProductPermissionPolicyTestCase(Pe
                          self.permsys.policies)
 
     def test_policy_chaining(self):
-        self.env.config.set('trac', 'permission_policies', 
+        self.env.config.set('trac', 'permission_policies',
                             'TestPermissionPolicy,DefaultPermissionPolicy')
         self.policy.grant('testuser', ['TEST_MODIFY'])
         system = perm.PermissionSystem(self.env)
@@ -346,7 +346,7 @@ class ProductPermissionPolicyTestCase(Pe
                             'Check for permission action %s' % (action,))
         self.assertFalse(self.perm.has_permission('UNKNOWN_PERM'))
 
-        # Clear permissions cache and retry 
+        # Clear permissions cache and retry
         self.perm._cache.clear()
         self.global_perm_admin._do_remove('testuser', 'TRAC_ADMIN')
 
@@ -371,9 +371,9 @@ class ProductPermissionPolicyTestCase(Pe
         # Setting TRAC_ADMIN permission in product scope is in vain
         # since it controls access to critical actions affecting the whole site
         # This will protect the system against malicious actors
-        # and / or failures leading to the addition of TRAC_ADMIN permission 
+        # and / or failures leading to the addition of TRAC_ADMIN permission
         # in product perm store in spite of obtaining unrighteous super powers.
-        # On the other hand this also means that PRODUCT_ADMIN(s) are 
+        # On the other hand this also means that PRODUCT_ADMIN(s) are
         # able to set user permissions at will without jeopardizing system
         # integrity and stability.
         self.product_perm_admin._do_add('testuser', 'TRAC_ADMIN')
@@ -439,7 +439,7 @@ class ProductPermissionPolicyTestCase(Pe
 
 def test_suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(ProductDefaultPermissionStoreTestCase, 
+    suite.addTest(unittest.makeSuite(ProductDefaultPermissionStoreTestCase,
                                      'test'))
     suite.addTest(unittest.makeSuite(ProductPermissionSystemTestCase, 'test'))
     suite.addTest(unittest.makeSuite(ProductPermissionCacheTestCase, 'test'))
@@ -452,4 +452,3 @@ def test_suite():
 
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
-