text: # -*- 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
  #  regarding copyright ownership.  The ASF licenses this file
  #  to you under the Apache License, Version 2.0 (the
  #  "License"); you may not use this file except in compliance
  #  with the License.  You may obtain a copy of the License at
  #
  #   http://www.apache.org/licenses/LICENSE-2.0
  #
  #  Unless required by applicable law or agreed to in writing,
  #  software distributed under the License is distributed on an
  #  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  #  KIND, either express or implied.  See the License for the
  #  specific language governing permissions and limitations
  #  under the License.
  
  """Tests for Apache(TM) Bloodhound's product configuration objects"""
  
  import os.path
  import shutil
  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
  from trac.util.text import to_unicode
  
  from multiproduct.api import MultiProductSystem
  from multiproduct.config import Configuration
  from multiproduct.model import Product, ProductSetting
  from tests.env import MultiproductTestCase
  
  class MultiproductConfigTestCase(MultiproductTestCase):
      r"""Test setup for configuration test cases.
      """
      def setUp(self):
          r"""Replace Trac environment with product environment
          """
          self.env = self._setup_test_env()
  
          # Dummy config file, a sibling of trac.ini
          tmpdir = os.path.realpath(self.env.path)
          self.filename = os.path.join(tmpdir, 'conf', 'product.ini')
          # Ensure conf sub-folder is created
          os.path.dirname(self.filename)
  
          self._upgrade_mp(self.env)
          self._setup_test_log(self.env)
          self._load_product_from_data(self.env, self.default_product)
          self._orig_registry = Option.registry
          Option.registry = {}
  
      def tearDown(self):
          Option.registry = self._orig_registry
          shutil.rmtree(self.env.path)
          self.env = None
  
      def _read(self, parents=None, product=None):
          r"""Override superclass method by returning product-aware configuration
          object retrieving settings from the database. Such objects will replace
          instances of `trac.config.Configuration` used in inherited test cases.
          """
          if product is None:
              product = self.default_product
          return Configuration(self.env, product, parents)
  
      def _write(self, lines, product=None):
          r"""Override superclass method by writing configuration values
          to the database rather than ini file in the filesystem.
          """
          if product is None:
              product = self.default_product
          product = to_unicode(product)
          fp = StringIO(('\n'.join(lines + [''])).encode('utf-8'))
          parser = ConfigParser()
          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,
                      {'product' : product}):
                  setting.delete()
              # Insert new options
              for section in parser.sections():
                  option_key = dict(
                          section=to_unicode(section),
                          product=to_unicode(product)
                      )
                  for option, value in parser.items(section):
                      option_key.update(dict(option=to_unicode(option)))
                      setting = ProductSetting(self.env)
                      setting._data.update(option_key)
                      setting._data['value'] = to_unicode(value)
                      setting.insert()
  
      def _test_with_inherit(self, testcb):
          """Almost exact copy of `trac.tests.config.ConfigurationTestCase`.
          Differences explained in inline comments.
          """
          # Parent configuration file created in environment's conf sub-folder
          # PS: This modification would not be necessary if the corresponding
          #     statement in overriden method would be written the same way
          #     but the fact that both files have the same parent folder
          #     is not made obvious in there
          sitename = os.path.join(os.path.dirname(self.filename), 'trac-site.ini')
  
          try:
              with open(sitename, 'w') as sitefile:
                  sitefile.write('[a]\noption = x\n')
  
              self._write(['[inherit]', 'file = trac-site.ini'])
              testcb()
          finally:
              os.remove(sitename)
  
      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
                  ProductSetting.select(config.env, where={'product' : product})]
  
          dump = []
          for section, group in groupby(sorted(rows), lambda row: row[0]):
              dump.append('[%s]\n' % (section,))
              for row in group:
                  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
  
      def test_set_and_save(self):
          config = self._read()
          config.set('b', u'öption0', 'y')
          config.set(u'aä', 'öption0', 'x')
          config.set('aä', 'option2', "Voilà l'été")  # UTF-8
          config.set(u'aä', 'option1', u"Voilà l'été") # unicode
          # Note: the following would depend on the locale.getpreferredencoding()
          # config.set('a', 'option3', "Voil\xe0 l'\xe9t\xe9") # latin-1
          self.assertEquals('x', config.get(u'aä', u'öption0'))
          self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option1'))
          self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option2'))
          config.save()
  
          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'[b]\n',
                             u'öption0 = y\n',
                             ],
                            dump)
          config2 = self._read()
          self.assertEquals('x', config2.get(u'aä', u'öption0'))
          self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option1'))
          self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option2'))
          # self.assertEquals(u"Voilà l'été", config2.get('a', 'option3'))
  
      def test_set_and_save_inherit(self):
          def testcb():
              config = self._read()
              config.set('a', 'option2', "Voilà l'été")  # UTF-8
              config.set('a', 'option1', u"Voilà l'été") # unicode
              self.assertEquals('x', config.get('a', 'option'))
              self.assertEquals(u"Voilà l'été", config.get('a', 'option1'))
              self.assertEquals(u"Voilà l'été", config.get('a', 'option2'))
              config.save()
  
              dump = self._dump_settings(config)
              self.assertEquals([
                                 u'[a]\n',
                                 u"option1 = Voilà l'été\n",
                                 u"option2 = Voilà l'été\n",
                                 u'[inherit]\n',
                                 u"file = trac-site.ini\n",
                                 ],
                                dump)
              config2 = self._read()
              self.assertEquals('x', config2.get('a', 'option'))
              self.assertEquals(u"Voilà l'été", config2.get('a', 'option1'))
              self.assertEquals(u"Voilà l'été", config2.get('a', 'option2'))
          self._test_with_inherit(testcb)
  
      def test_overwrite(self):
          config = self._read()
          config.set('a', 'option', 'value1')
          self.assertEquals('value1', config.get('a', 'option'))
          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():
      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')
  