BIND 10 master, updated. e606d8d0e66a4522640c3bf6ae53b787a7419c45 [master] Merge branch 'trac2689'

BIND 10 source code commits bind10-changes at lists.isc.org
Mon Feb 25 22:13:38 UTC 2013


The branch, master has been updated
       via  e606d8d0e66a4522640c3bf6ae53b787a7419c45 (commit)
       via  d6e34fc0d99e325ccd5f253abdbb1abd27b21935 (commit)
       via  6ffab5a7aa42d2f1210d3036658dcf4398c6ff99 (commit)
       via  3fa16c71cbffab13bcdeb311a1e81a29f107d647 (commit)
       via  808dcdccc72caa04645c15af4493a76bacbd838f (commit)
       via  ba2982025bc36dbf61f1268a6c0c884b2a669127 (commit)
       via  5adf1f39d4b8060cd5cc7ab764f94ec6faaf5193 (commit)
       via  2c66b4437daf5d4ee493cb328d02eba49d1bbc70 (commit)
       via  0f795237b2b0384cde3a60bb49df75cd529e31bc (commit)
       via  f8e0a4f7e6fde7d1fe2c63a0bebfe00a2e2e7164 (commit)
       via  c8aea8a73e16028bb07d51caafe5bf8188d0ee14 (commit)
       via  107f0d258ab020c6ef131a0c6d682c1894fb85a3 (commit)
       via  484cc29669ae8eaf5270802cc66cbe0daaf24b1d (commit)
       via  5ba7df48ebcf5759d87e4e487aca9deb35799b1b (commit)
       via  521c12d55ea5df2b98ef0543bc78e8d2e40c8a4b (commit)
       via  17242d96b692893b0fe127c0f4883d30a765e58e (commit)
       via  146d0e9b8a417220a9a9eeb9adb4c2ccb66db178 (commit)
       via  26a7d162b701bb210d4394348d35fa8090da2733 (commit)
      from  4c9255afa0727284de95283783d5e40dc7341606 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit e606d8d0e66a4522640c3bf6ae53b787a7419c45
Merge: 4c9255a d6e34fc
Author: JINMEI Tatuya <jinmei at isc.org>
Date:   Mon Feb 25 14:12:18 2013 -0800

    [master] Merge branch 'trac2689'

-----------------------------------------------------------------------

Summary of changes:
 src/bin/stats/stats.py.in                   |   94 +++---
 src/bin/stats/tests/b10-stats-httpd_test.py |   17 +-
 src/bin/stats/tests/b10-stats_test.py       |  429 +++++++++++++++++----------
 src/bin/stats/tests/test_utils.py           |  154 +++++++++-
 4 files changed, 477 insertions(+), 217 deletions(-)

-----------------------------------------------------------------------
diff --git a/src/bin/stats/stats.py.in b/src/bin/stats/stats.py.in
index 9ace6c8..577afe6 100755
--- a/src/bin/stats/stats.py.in
+++ b/src/bin/stats/stats.py.in
@@ -190,12 +190,19 @@ class Stats:
     """
     Main class of stats module
     """
-    def __init__(self):
+    def __init__(self, module_ccsession_class=isc.config.ModuleCCSession):
+        '''Constructor
+
+        module_ccsession_class is parameterized so that test can specify
+        a mocked class to test the behavior without involing network I/O.
+        In other cases this parameter shouldn't be specified.
+
+        '''
         self.running = False
         # create ModuleCCSession object
-        self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
-                                               self.config_handler,
-                                               self.command_handler)
+        self.mccs = module_ccsession_class(SPECFILE_LOCATION,
+                                           self.config_handler,
+                                           self.command_handler)
         self.cc_session = self.mccs._session
         # get module spec
         self.module_name = self.mccs.get_module_spec().get_module_name()
@@ -225,7 +232,16 @@ class Stats:
                 ])
         # set a absolute timestamp polling at next time
         self.next_polltime = get_timestamp() + self.get_interval()
-        # initialized Statistics data
+
+        self._init_statistics_data()
+
+    def _init_statistics_data(self):
+        """initialized Statistics data.
+
+        This method is a dedicated subroutine of __int__(), but extracted
+        so tests can override it to avoid blocking network operation.
+
+        """
         self.update_modules()
         if self.update_statistics_data(
             self.module_name,
@@ -316,11 +332,9 @@ class Stats:
         while len(sequences) > 0:
             try:
                 (module_name, seq) = sequences.pop(0)
-                answer, env = self.cc_session.group_recvmsg(
-                    False, seq)
+                answer, env = self.cc_session.group_recvmsg(False, seq)
                 if answer:
-                    rcode, args = isc.config.ccsession.parse_answer(
-                        answer)
+                    rcode, args = isc.config.ccsession.parse_answer(answer)
                     if rcode == 0:
                         _statistics_data.append(
                             (module_name, env['from'], args))
@@ -347,31 +361,35 @@ class Stats:
         # if successfully done, set the last time of polling
         self._lasttime_poll = get_timestamp()
 
+    def _check_command(self, nonblock=False):
+        """check invoked command by waiting for 'poll-interval' seconds
+
+        This is a dedicated subroutine of start(), but extracted and defined
+        as a 'protected' method so that tests can replace it.
+
+        """
+        # backup original timeout
+        orig_timeout = self.cc_session.get_timeout()
+        # set cc-session timeout to half of a second(500ms)
+        self.cc_session.set_timeout(500)
+        try:
+            answer, env = self.cc_session.group_recvmsg(nonblock)
+            self.mccs.check_command_without_recvmsg(answer, env)
+        except isc.cc.session.SessionTimeout:
+            pass # waited for poll-interval seconds
+        # restore timeout
+        self.cc_session.set_timeout(orig_timeout)
+
     def start(self):
         """
         Start stats module
         """
         logger.info(STATS_STARTING)
 
-        def _check_command(nonblock=False):
-            """check invoked command by waiting for 'poll-interval'
-            seconds"""
-            # backup original timeout
-            orig_timeout = self.cc_session.get_timeout()
-            # set cc-session timeout to half of a second(500ms)
-            self.cc_session.set_timeout(500)
-            try:
-                answer, env = self.cc_session.group_recvmsg(nonblock)
-                self.mccs.check_command_without_recvmsg(answer, env)
-            except isc.cc.session.SessionTimeout:
-                pass # waited for poll-interval seconds
-            # restore timeout
-            self.cc_session.set_timeout(orig_timeout)
-
         try:
             self.running = True
             while self.running:
-                _check_command()
+                self._check_command()
                 now = get_timestamp()
                 intval = self.get_interval()
                 if intval > 0 and now >= self.next_polltime:
@@ -476,16 +494,16 @@ class Stats:
         updates statistics data. If specified data is invalid for
         statistics spec of specified owner, it returns a list of error
         messages. If there is no error or if neither owner nor data is
-        specified in args, it returns None. The 'mid' argument is an identifier of
-        the sender module in order for stats to identify which
+        specified in args, it returns None. The 'mid' argument is an
+        identifier of the sender module in order for stats to identify which
         instance sends statistics data in the situation that multiple
         instances are working.
         """
         # Note:
         # The fix of #1751 is for multiple instances working. It is
         # assumed here that they send different statistics data with
-        # each sender module id (mid). Stats should save their statistics data by
-        # mid. The statistics data, which is the existing variable, is
+        # each sender module id (mid). Stats should save their statistics data
+        # by mid. The statistics data, which is the existing variable, is
         # preserved by accumlating from statistics data by the mid. This
         # is an ad-hoc fix because administrators can not see
         # statistics by each instance via bindctl or HTTP/XML. These
@@ -535,8 +553,7 @@ class Stats:
                             # merge recursively old value and new
                             # value each other
                             _data[owner][mid] = \
-                                merge_oldnew(_data[owner][mid],
-                                             {_key: _val})
+                                merge_oldnew(_data[owner][mid], {_key: _val})
                         continue
                     # the key string might be a "xx/yy/zz[0]"
                     # type. try it.
@@ -546,22 +563,20 @@ class Stats:
                         if errors: errors.pop()
                         # try updata and check validation in adavance
                         __data = _data.copy()
-                        if owner not in _data:
+                        if owner not in __data:
                             __data[owner] = {}
-                        if mid not in _data[owner]:
+                        if mid not in __data[owner]:
                             __data[owner][mid] = {}
                         # use the isc.cc.data.set method
                         try:
-                            isc.cc.data.set(__data[owner][mid],
-                                            _key, _val)
+                            isc.cc.data.set(__data[owner][mid], _key, _val)
                             if self.modules[owner].validate_statistics(
                                 False, __data[owner][mid], errors):
                                 _data = __data
                         except Exception as e:
-                            errors.append(
-                                "%s: %s" % (e.__class__.__name__, e))
+                            errors.append("%s: %s" % (e.__class__.__name__, e))
             except KeyError:
-                errors.append("unknown module name: " + str(owner))
+                errors.append("unknown module name: " + owner)
             if not errors:
                 self.statistics_data_bymid = _data
 
@@ -584,8 +599,7 @@ class Stats:
                     # values are not replaced.
                     self.statistics_data[m] = merge_oldnew(
                         self.statistics_data[m],
-                        _accum_bymodule(
-                            self.statistics_data_bymid[m]))
+                        _accum_bymodule(self.statistics_data_bymid[m]))
 
         if errors: return errors
 
diff --git a/src/bin/stats/tests/b10-stats-httpd_test.py b/src/bin/stats/tests/b10-stats-httpd_test.py
index 961986e..466e448 100644
--- a/src/bin/stats/tests/b10-stats-httpd_test.py
+++ b/src/bin/stats/tests/b10-stats-httpd_test.py
@@ -48,7 +48,7 @@ import stats_httpd
 import stats
 from test_utils import BaseModules, ThreadingServerManager, MyStats,\
                        MyStatsHttpd, SignalHandler,\
-                       send_command, send_shutdown, CONST_BASETIME
+                       send_command, CONST_BASETIME
 from isc.testutils.ccsession_mock import MockModuleCCSession
 
 # This test suite uses xml.etree.ElementTree.XMLParser via
@@ -461,7 +461,8 @@ class TestHttpHandler(unittest.TestCase):
                          (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
         # failure case(Stats is down)
         self.assertTrue(self.stats.running)
-        self.assertEqual(send_shutdown("Stats"), (0, None)) # Stats is down
+        self.assertEqual(send_command("shutdown", "Stats"),
+                         (0, None)) # Stats is down
         self.assertFalse(self.stats.running)
         self.stats_httpd.cc_session.set_timeout(milliseconds=100)
 
@@ -608,8 +609,16 @@ class TestStatsHttpd(unittest.TestCase):
         self.stats_server.run()
         # checking IPv6 enabled on this platform
         self.ipv6_enabled = is_ipv6_enabled()
+        # instantiation of StatsHttpd indirectly calls gethostbyaddr(), which
+        # can block for an uncontrollable period, leading many undesirable
+        # results.  We should rather eliminate the reliance, but until we
+        # can make such fundamental cleanup we replace it with a faked method;
+        # in our test scenario the return value doesn't matter.
+        self.__gethostbyaddr_orig = socket.gethostbyaddr
+        socket.gethostbyaddr = lambda x: ('test.example.', [], None)
 
     def tearDown(self):
+        socket.gethostbyaddr = self.__gethostbyaddr_orig
         if hasattr(self, "stats_httpd"):
             self.stats_httpd.stop()
         self.stats_server.shutdown()
@@ -751,7 +760,7 @@ class TestStatsHttpd(unittest.TestCase):
         self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, server_addresses)
         self.stats_httpd_server.run()
         self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, server_addresses)
-        send_shutdown("StatsHttpd")
+        send_command("shutdown", "StatsHttpd")
 
     def test_running(self):
         self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, get_availaddr())
@@ -761,7 +770,7 @@ class TestStatsHttpd(unittest.TestCase):
         self.assertEqual(send_command("status", "StatsHttpd"),
                          (0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
         self.assertTrue(self.stats_httpd.running)
-        self.assertEqual(send_shutdown("StatsHttpd"), (0, None))
+        self.assertEqual(send_command("shutdown", "StatsHttpd"), (0, None))
         self.assertFalse(self.stats_httpd.running)
         self.stats_httpd_server.shutdown()
 
diff --git a/src/bin/stats/tests/b10-stats_test.py b/src/bin/stats/tests/b10-stats_test.py
index b76e4d2..7732902 100644
--- a/src/bin/stats/tests/b10-stats_test.py
+++ b/src/bin/stats/tests/b10-stats_test.py
@@ -32,7 +32,8 @@ import sys
 import stats
 import isc.log
 import isc.cc.session
-from test_utils import BaseModules, ThreadingServerManager, MyStats, SignalHandler, send_command, send_shutdown
+from test_utils import BaseModules, ThreadingServerManager, MyStats, \
+    SimpleStats, SignalHandler, MyModuleCCSession, send_command
 from isc.testutils.ccsession_mock import MockModuleCCSession
 
 class TestUtilties(unittest.TestCase):
@@ -247,11 +248,17 @@ class TestStats(unittest.TestCase):
         self.const_timestamp = 1308730448.965706
         self.const_datetime = '2011-06-22T08:14:08Z'
         self.const_default_datetime = '1970-01-01T00:00:00Z'
+        # Record original module-defined functions in case we replace them
+        self.__orig_timestamp = stats.get_timestamp
+        self.__orig_get_datetime = stats.get_datetime
 
     def tearDown(self):
         self.base.shutdown()
         # reset the signal handler
         self.sig_handler.reset()
+        # restore the stored original function in case we replaced them
+        stats.get_timestamp = self.__orig_timestamp
+        stats.get_datetime = self.__orig_get_datetime
 
     def test_init(self):
         self.stats = stats.Stats()
@@ -287,133 +294,212 @@ class TestStats(unittest.TestCase):
         self.assertRaises(stats.StatsError, stats.Stats)
         stats.SPECFILE_LOCATION = orig_spec_location
 
+    def __send_command(self, stats, command_name, params=None):
+        '''Emulate a command arriving to stats by directly calling callback'''
+        return isc.config.ccsession.parse_answer(
+            stats.command_handler(command_name, params))
+
     def test_start(self):
+        # Define a separate exception class so we can be sure that's actually
+        # the one raised in __check_start() below
+        class CheckException(Exception):
+            pass
+
+        def __check_start(tested_stats):
+            self.assertTrue(tested_stats.running)
+            raise CheckException # terminate the loop
+
         # start without err
-        self.stats_server = ThreadingServerManager(MyStats)
-        self.stats = self.stats_server.server
-        self.assertFalse(self.stats.running)
-        self.stats_server.run()
-        self.assertEqual(send_command("status", "Stats"),
-                (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
-        self.assertTrue(self.stats.running)
-        # Due to a race-condition related to the threads used in these
-        # tests, use of the mock session and the stopped check (see
-        # below), are temporarily disabled
-        # See ticket #1668
-        # Override moduleCCSession so we can check if send_stopping is called
-        #self.stats.mccs = MockModuleCCSession()
-        self.assertEqual(send_shutdown("Stats"), (0, None))
-        self.assertFalse(self.stats.running)
-        # Call server.shutdown with argument True so the thread.join() call
-        # blocks and we are sure the main loop has finished (and set
-        # mccs.stopped)
-        self.stats_server.shutdown(True)
-        # Also temporarily disabled for #1668, see above
-        #self.assertTrue(self.stats.mccs.stopped)
+        stats = SimpleStats()
+        self.assertFalse(stats.running)
+        stats._check_command = lambda: __check_start(stats)
+        # We are going to confirm start() will set running to True, avoiding
+        # to fall into a loop with the exception trick.
+        self.assertRaises(CheckException, stats.start)
+        self.assertEqual(self.__send_command(stats, "status"),
+                         (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
+
+    def test_shutdown(self):
+        def __check_shutdown(tested_stats):
+            self.assertTrue(tested_stats.running)
+            self.assertEqual(self.__send_command(tested_stats, "shutdown"),
+                             (0, None))
+            self.assertFalse(tested_stats.running)
+            # override get_interval() so it won't go poll statistics
+            tested_stats.get_interval = lambda : 0
+
+        stats = SimpleStats()
+        stats._check_command = lambda: __check_shutdown(stats)
+        stats.start()
+        self.assertTrue(stats.mccs.stopped)
 
     def test_handlers(self):
-        self.stats_server = ThreadingServerManager(MyStats)
-        self.stats = self.stats_server.server
-        self.stats_server.run()
+        """Test command_handler"""
 
-        # command_handler
+        __stats = SimpleStats()
+
+        # 'show' command.  We're going to check the expected methods are
+        # called in the expected order, and check the resulting response.
+        # Details of each method are tested separately.
+        call_log = []
+        def __steal_method(fn_name, *arg):
+            call_log.append((fn_name, arg))
+            if fn_name == 'update_stat':
+                return False        # "no error"
+            if fn_name == 'showschema':
+                return isc.config.create_answer(0, 'no error')
+
+        # Fake some methods and attributes for inspection
+        __stats.do_polling = lambda: __steal_method('polling')
+        __stats.update_statistics_data = \
+            lambda x, y, z: __steal_method('update_stat', x, y, z)
+        __stats.update_modules = lambda: __steal_method('update_module')
+        __stats.mccs.lname = 'test lname'
+        __stats.statistics_data = {'Init': {'boot_time': self.const_datetime}}
+
+        # skip initial polling
+        stats.get_timestamp = lambda: 0
+        __stats._lasttime_poll = 0
+
+        stats.get_datetime = lambda: 42 # make the result predictable
+
+        # now send the command
         self.assertEqual(
-            send_command(
-                'show', 'Stats',
-                params={ 'owner' : 'Init',
-                  'name'  : 'boot_time' }),
+            self.__send_command(
+                __stats, 'show',
+                params={ 'owner' : 'Init', 'name'  : 'boot_time' }),
             (0, {'Init': {'boot_time': self.const_datetime}}))
+        # Check if expected methods are called
+        self.assertEqual([('update_stat',
+                           ('Stats', 'test lname',
+                            {'timestamp': 0,
+                             'report_time': 42})),
+                          ('update_module', ())], call_log)
+
+        # Then update faked timestamp so the intial polling will happen, and
+        # confirm that.
+        call_log = []
+        stats.get_timestamp = lambda: 10
         self.assertEqual(
-            send_command(
-                'show', 'Stats',
-                params={ 'owner' : 'Init',
-                  'name'  : 'boot_time' }),
+            self.__send_command(
+                __stats, 'show',
+                params={ 'owner' : 'Init', 'name'  : 'boot_time' }),
             (0, {'Init': {'boot_time': self.const_datetime}}))
+        self.assertEqual([('polling', ()),
+                          ('update_stat',
+                           ('Stats', 'test lname',
+                            {'timestamp': 10,
+                             'report_time': 42})),
+                          ('update_module', ())], call_log)
+
+        # 'status' command.  We can confirm the behavior without any fake
         self.assertEqual(
-            send_command('status', 'Stats'),
+            self.__send_command(__stats, 'status'),
             (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
 
-        (rcode, value) = send_command('show', 'Stats')
-        self.assertEqual(rcode, 0)
-        self.assertEqual(len(value), 3)
-        self.assertTrue('Init' in value)
-        self.assertTrue('Stats' in value)
-        self.assertTrue('Auth' in value)
-        self.assertEqual(len(value['Stats']), 5)
-        self.assertEqual(len(value['Init']), 1)
-        self.assertTrue('boot_time' in value['Init'])
-        self.assertEqual(value['Init']['boot_time'], self.const_datetime)
-        self.assertTrue('report_time' in value['Stats'])
-        self.assertTrue('boot_time' in value['Stats'])
-        self.assertTrue('last_update_time' in value['Stats'])
-        self.assertTrue('timestamp' in value['Stats'])
-        self.assertTrue('lname' in value['Stats'])
-        (rcode, value) = send_command('showschema', 'Stats')
-        self.assertEqual(rcode, 0)
-        self.assertEqual(len(value), 3)
-        self.assertTrue('Init' in value)
-        self.assertTrue('Stats' in value)
-        self.assertTrue('Auth' in value)
-        self.assertEqual(len(value['Stats']), 5)
-        self.assertEqual(len(value['Init']), 1)
-        for item in value['Init']:
-            self.assertTrue(len(item) == 7)
-            self.assertTrue('item_name' in item)
-            self.assertTrue('item_type' in item)
-            self.assertTrue('item_optional' in item)
-            self.assertTrue('item_default' in item)
-            self.assertTrue('item_title' in item)
-            self.assertTrue('item_description' in item)
-            self.assertTrue('item_format' in item)
-        for item in value['Stats']:
-            self.assertTrue(len(item) == 6 or len(item) == 7)
-            self.assertTrue('item_name' in item)
-            self.assertTrue('item_type' in item)
-            self.assertTrue('item_optional' in item)
-            self.assertTrue('item_default' in item)
-            self.assertTrue('item_title' in item)
-            self.assertTrue('item_description' in item)
-            if len(item) == 7:
-                self.assertTrue('item_format' in item)
+        # 'showschema' command.  update_modules() will be called, which
+        # (implicitly) cofirms the correct method is called; further details
+        # are tested seprately.
+        call_log = []
+        (rcode, value) = self.__send_command(__stats, 'showschema')
+        self.assertEqual([('update_module', ())], call_log)
 
+        # Unknown command.  Error should be returned
         self.assertEqual(
-            send_command('__UNKNOWN__', 'Stats'),
+            self.__send_command(__stats, '__UNKNOWN__'),
             (1, "Unknown command: '__UNKNOWN__'"))
 
-        self.stats_server.shutdown()
-
     def test_update_modules(self):
-        self.stats = stats.Stats()
-        self.assertEqual(len(self.stats.modules), 3) # Auth, Init, Stats
+        """Confirm the behavior of Stats.update_modules().
+
+        It checks whether the expected command is sent to ConfigManager,
+        and whether the answer from ConfigManager is handled as expected.
+
+        """
+
+        def __check_rpc_call(command, group):
+            self.assertEqual('ConfigManager', group)
+            self.assertEqual(command,
+                             isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC)
+            answer_value = {'Init': [{
+                        "item_name": "boot_time",
+                        "item_type": "string",
+                        "item_optional": False,
+                        # Use a different default so we can check it below
+                        "item_default": "2013-01-01T00:00:01Z",
+                        "item_title": "Boot time",
+                        "item_description": "dummy desc",
+                        "item_format": "date-time"
+                        }]}
+            return answer_value
+
+        self.stats = SimpleStats()
+        self.stats.cc_session.rpc_call = __check_rpc_call
+
         self.stats.update_modules()
+
+        # Stats is always incorporated.  For others, only the ones returned
+        # by group_recvmsg() above is available.
         self.assertTrue('Stats' in self.stats.modules)
         self.assertTrue('Init' in self.stats.modules)
         self.assertFalse('Dummy' in self.stats.modules)
-        my_statistics_data = stats.get_spec_defaults(self.stats.modules['Stats'].get_statistics_spec())
+
+        my_statistics_data = stats.get_spec_defaults(
+            self.stats.modules['Stats'].get_statistics_spec())
         self.assertTrue('report_time' in my_statistics_data)
         self.assertTrue('boot_time' in my_statistics_data)
         self.assertTrue('last_update_time' in my_statistics_data)
         self.assertTrue('timestamp' in my_statistics_data)
         self.assertTrue('lname' in my_statistics_data)
-        self.assertEqual(my_statistics_data['report_time'], self.const_default_datetime)
-        self.assertEqual(my_statistics_data['boot_time'], self.const_default_datetime)
-        self.assertEqual(my_statistics_data['last_update_time'], self.const_default_datetime)
+        self.assertEqual(my_statistics_data['report_time'],
+                         self.const_default_datetime)
+        self.assertEqual(my_statistics_data['boot_time'],
+                         self.const_default_datetime)
+        self.assertEqual(my_statistics_data['last_update_time'],
+                         self.const_default_datetime)
         self.assertEqual(my_statistics_data['timestamp'], 0.0)
         self.assertEqual(my_statistics_data['lname'], "")
-        my_statistics_data = stats.get_spec_defaults(self.stats.modules['Init'].get_statistics_spec())
+        my_statistics_data = stats.get_spec_defaults(
+            self.stats.modules['Init'].get_statistics_spec())
         self.assertTrue('boot_time' in my_statistics_data)
-        self.assertEqual(my_statistics_data['boot_time'], self.const_default_datetime)
+        self.assertEqual(my_statistics_data['boot_time'],
+                         "2013-01-01T00:00:01Z")
+
+        # Error case
+        def __raise_on_rpc_call(x, y):
+            raise isc.config.RPCError(99, 'error')
         orig_parse_answer = stats.isc.config.ccsession.parse_answer
-        stats.isc.config.ccsession.parse_answer = lambda x: (99, 'error')
+        self.stats.cc_session.rpc_call = __raise_on_rpc_call
         self.assertRaises(stats.StatsError, self.stats.update_modules)
-        stats.isc.config.ccsession.parse_answer = orig_parse_answer
 
     def test_get_statistics_data(self):
-        self.stats = stats.Stats()
+        """Confirm the behavior of Stats.get_statistics_data().
+
+        It should first call update_modules(), and then retrieve the requested
+        data from statistics_data.  We confirm this by fake update_modules()
+        where we set the expected data in statistics_data.
+
+        """
+        self.stats = SimpleStats()
+        def __faked_update_modules():
+            self.stats.statistics_data = { \
+                'Stats': {
+                    'report_time': self.const_default_datetime,
+                    'boot_time': None,
+                    'last_update_time': None,
+                    'timestamp': 0.0,
+                    'lname': 'dummy name'
+                    },
+                'Init': { 'boot_time': None }
+                }
+
+        self.stats.update_modules = __faked_update_modules
+
         my_statistics_data = self.stats.get_statistics_data()
         self.assertTrue('Stats' in my_statistics_data)
         self.assertTrue('Init' in my_statistics_data)
         self.assertTrue('boot_time' in my_statistics_data['Init'])
+
         my_statistics_data = self.stats.get_statistics_data(owner='Stats')
         self.assertTrue('Stats' in my_statistics_data)
         self.assertTrue('report_time' in my_statistics_data['Stats'])
@@ -421,16 +507,28 @@ class TestStats(unittest.TestCase):
         self.assertTrue('last_update_time' in my_statistics_data['Stats'])
         self.assertTrue('timestamp' in my_statistics_data['Stats'])
         self.assertTrue('lname' in my_statistics_data['Stats'])
-        self.assertRaises(stats.StatsError, self.stats.get_statistics_data, owner='Foo')
-        my_statistics_data = self.stats.get_statistics_data(owner='Stats', name='report_time')
-        self.assertEqual(my_statistics_data['Stats']['report_time'], self.const_default_datetime)
-        my_statistics_data = self.stats.get_statistics_data(owner='Stats', name='boot_time')
+        self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
+                          owner='Foo')
+
+        my_statistics_data = self.stats.get_statistics_data(
+            owner='Stats', name='report_time')
+        self.assertEqual(my_statistics_data['Stats']['report_time'],
+                         self.const_default_datetime)
+
+        my_statistics_data = self.stats.get_statistics_data(
+            owner='Stats', name='boot_time')
         self.assertTrue('boot_time' in my_statistics_data['Stats'])
-        my_statistics_data = self.stats.get_statistics_data(owner='Stats', name='last_update_time')
+
+        my_statistics_data = self.stats.get_statistics_data(
+            owner='Stats', name='last_update_time')
         self.assertTrue('last_update_time' in my_statistics_data['Stats'])
-        my_statistics_data = self.stats.get_statistics_data(owner='Stats', name='timestamp')
+
+        my_statistics_data = self.stats.get_statistics_data(
+            owner='Stats', name='timestamp')
         self.assertEqual(my_statistics_data['Stats']['timestamp'], 0.0)
-        my_statistics_data = self.stats.get_statistics_data(owner='Stats', name='lname')
+
+        my_statistics_data = self.stats.get_statistics_data(
+            owner='Stats', name='lname')
         self.assertTrue(len(my_statistics_data['Stats']['lname']) >0)
         self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
                           owner='Stats', name='Bar')
@@ -441,7 +539,7 @@ class TestStats(unittest.TestCase):
 
     def test_update_statistics_data(self):
         """test for list-type statistics"""
-        self.stats = stats.Stats()
+        self.stats = SimpleStats()
         _test_exp1 = {
               'zonename': 'test1.example',
               'queries.tcp': 5,
@@ -518,42 +616,20 @@ class TestStats(unittest.TestCase):
 
     def test_update_statistics_data_pt2(self):
         """test for named_set-type statistics"""
-        self.stats = stats.Stats()
-        self.stats.do_polling()
-        _test_exp1 = {
-              'test10.example': {
-                  'queries.tcp': 5,
-                  'queries.udp': 4
-              }
-            }
-        _test_exp2 = {
-              'test20.example': {
-                  'queries.tcp': 3,
-                  'queries.udp': 2
-              }
-            }
+        self.stats = SimpleStats()
+        _test_exp1 = \
+            { 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
+        _test_exp2 = \
+            { 'test20.example': { 'queries.tcp': 3, 'queries.udp': 2 } }
         _test_exp3 = {}
-        _test_exp4 = {
-              'test20.example': {
-                  'queries.udp': 4
-              }
-            }
-        _test_exp5_1 = {
-              'test10.example': {
-                 'queries.udp': 5432
-              }
-            }
+        _test_exp4 = { 'test20.example': { 'queries.udp': 4 } }
+        _test_exp5_1 = { 'test10.example': { 'queries.udp': 5432 } }
         _test_exp5_2 ={
               'nds_queries.perzone/test10.example/queries.udp':
-                  isc.cc.data.find(_test_exp5_1,
-                                   'test10.example/queries.udp')
-            }
-        _test_exp6 = {
-              'foo/bar':  'brabra'
-            }
-        _test_exp7 = {
-              'foo[100]': 'bar'
+                  isc.cc.data.find(_test_exp5_1, 'test10.example/queries.udp')
             }
+        _test_exp6 = { 'foo/bar':  'brabra' }
+        _test_exp7 = { 'foo[100]': 'bar' }
         # Success cases
         self.assertIsNone(self.stats.update_statistics_data(
             'Auth', 'foo1', {'nds_queries.perzone': _test_exp1}))
@@ -566,13 +642,15 @@ class TestStats(unittest.TestCase):
                              ['foo1']['nds_queries.perzone'],\
                          dict(_test_exp1,**_test_exp2))
         self.assertIsNone(self.stats.update_statistics_data(
-            'Auth', 'foo1', {'nds_queries.perzone': dict(_test_exp1,**_test_exp2)}))
+            'Auth', 'foo1', {'nds_queries.perzone':
+                                 dict(_test_exp1, **_test_exp2)}))
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
                              ['foo1']['nds_queries.perzone'],
-                         dict(_test_exp1,**_test_exp2))
+                         dict(_test_exp1, **_test_exp2))
         # differential update
         self.assertIsNone(self.stats.update_statistics_data(
-            'Auth', 'foo1', {'nds_queries.perzone': dict(_test_exp3,**_test_exp4)}))
+            'Auth', 'foo1', {'nds_queries.perzone':
+                                 dict(_test_exp3, **_test_exp4)}))
         _new_val = dict(_test_exp1,
                         **stats.merge_oldnew(_test_exp2,_test_exp4))
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
@@ -592,7 +670,8 @@ class TestStats(unittest.TestCase):
                              _test_exp5_1)
         # Error cases
         self.assertEqual(self.stats.update_statistics_data(
-                'Auth', 'foo1', {'nds_queries.perzone': None}), ['None should be a map'])
+                'Auth', 'foo1', {'nds_queries.perzone': None}),
+                         ['None should be a map'])
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
                              ['foo1']['nds_queries.perzone'],\
                              _new_val)
@@ -606,33 +685,57 @@ class TestStats(unittest.TestCase):
         self.assertEqual(self.stats.update_statistics_data(
                 'Foo', 'foo1', _test_exp6), ['unknown module name: Foo'])
 
-    @unittest.skipIf(sys.version_info >= (3, 3), "Unsupported in Python 3.3 or higher")
     def test_update_statistics_data_withmid(self):
-        self.stats = stats.Stats()
+        self.stats = SimpleStats()
+
+        # This test relies on existing statistics data at the Stats object.
+        # This version of test prepares the data using the do_polling() method;
+        # that's a bad practice because a unittest for a method
+        # (update_statistics_data) would heavily depend on details of another
+        # method (do_polling).  However, there's currently no direct test
+        # for do_polling (which is also bad), so we still keep that approach,
+        # partly for testing do_polling indirectly.  #2781 should provide
+        # direct test for do_polling, with which this test scenario should
+        # also be changed to be more stand-alone.
+
+        # We use the knowledge of what kind of messages are sent via
+        # do_polling, and return the following faked answer directly.
+        create_answer = isc.config.ccsession.create_answer # shortcut
+        self.stats._answers = [\
+            # Answer for "show_processes"
+            (create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
+                               [1035, 'b10-auth-2', 'Auth']]),  None),
+            # Answers for "getstats".  2 for Auth instances and 1 for Init.
+            # we return some bogus values for Init, but the rest of the test
+            # doesn't need it, so it's okay.
+            (create_answer(0, self.stats._auth_sdata), {'from': 'auth1'}),
+            (create_answer(0, self.stats._auth_sdata), {'from': 'auth2'}),
+            (create_answer(0, self.stats._auth_sdata), {'from': 'auth3'})
+            ]
+        # do_polling calls update_modules internally; in our scenario there's
+        # no change in modules, so we make it no-op.
+        self.stats.update_modules = lambda: None
+        # Now call do_polling.
         self.stats.do_polling()
+
         # samples of query number
         bar1_tcp = 1001
         bar2_tcp = 2001
         bar3_tcp = 1002
         bar3_udp = 1003
-        # two auth instances invoked
-        list_auth = [ self.base.auth.server,
-                      self.base.auth2.server ]
-        sum_qtcp = 0
-        for a in list_auth:
-            sum_qtcp += a.queries_tcp
-        sum_qudp = 0
-        for a in list_auth:
-            sum_qudp += a.queries_udp
+        # two auth instances invoked, so we double the pre-set stat values
+        sum_qtcp = self.stats._queries_tcp * 2
+        sum_qudp = self.stats._queries_udp * 2
         self.stats.update_statistics_data('Auth', "bar1 at foo",
-                                          {'queries.tcp':bar1_tcp})
+                                          {'queries.tcp': bar1_tcp})
         self.assertTrue('Auth' in self.stats.statistics_data)
         self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
         self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
                          bar1_tcp + sum_qtcp)
         self.assertTrue('Auth' in self.stats.statistics_data_bymid)
         self.assertTrue('bar1 at foo' in self.stats.statistics_data_bymid['Auth'])
-        self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid['Auth']['bar1 at foo'])
+        self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid
+                        ['Auth']['bar1 at foo'])
         self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar1 at foo'],
                          {'queries.tcp': bar1_tcp})
         # check consolidation of statistics data even if there is
@@ -658,7 +761,8 @@ class TestStats(unittest.TestCase):
         self.assertTrue('queries.udp' in self.stats.statistics_data['Auth'])
         self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
                          bar1_tcp + bar2_tcp + sum_qtcp)
-        self.assertEqual(self.stats.statistics_data['Auth']['queries.udp'], sum_qudp)
+        self.assertEqual(self.stats.statistics_data['Auth']['queries.udp'],
+                         sum_qudp)
         self.assertTrue('Auth' in self.stats.statistics_data_bymid)
         # restore statistics data of killed auth
         # self.base.b10_init.server.pid_list = [ killed ] + self.base.b10_init.server.pid_list[:]
@@ -690,8 +794,8 @@ class TestStats(unittest.TestCase):
     def test_config(self):
         orig_get_timestamp = stats.get_timestamp
         stats.get_timestamp = lambda : self.const_timestamp
-        stats_server = ThreadingServerManager(MyStats)
-        stat = stats_server.server
+        stat = SimpleStats()
+
         # test updating poll-interval
         self.assertEqual(stat.config['poll-interval'], 60)
         self.assertEqual(stat.get_interval(), 60)
@@ -715,14 +819,25 @@ class TestStats(unittest.TestCase):
         self.assertEqual(stat.config_handler({'poll-interval': 0}),
                          isc.config.create_answer(0))
         self.assertEqual(stat.config['poll-interval'], 0)
-        stats_server.run()
+
+        # see the comment for test_update_statistics_data_withmid.  We abuse
+        # do_polling here, too.  With #2781 we should make it more direct.
+        create_answer = isc.config.ccsession.create_answer # shortcut
+        stat._answers = [\
+            # Answer for "show_processes"
+            (create_answer(0, []),  None),
+            # Answers for "getstats" for Init (the other one for Auth, but
+            # that doesn't matter for this test)
+            (create_answer(0, stat._init_sdata), {'from': 'init'}),
+            (create_answer(0, stat._init_sdata), {'from': 'init'})
+            ]
+        stat.update_modules = lambda: None
+
         self.assertEqual(
-            send_command(
-                'show', 'Stats',
-                params={ 'owner' : 'Init',
-                  'name'  : 'boot_time' }),
+            self.__send_command(
+                stat, 'show',
+                params={ 'owner' : 'Init', 'name'  : 'boot_time' }),
             (0, {'Init': {'boot_time': self.const_datetime}}))
-        stats_server.shutdown()
 
     def test_commands(self):
         self.stats = stats.Stats()
diff --git a/src/bin/stats/tests/test_utils.py b/src/bin/stats/tests/test_utils.py
index 1c5cc3c..bfabc13 100644
--- a/src/bin/stats/tests/test_utils.py
+++ b/src/bin/stats/tests/test_utils.py
@@ -51,30 +51,18 @@ class SignalHandler():
         """envokes unittest.TestCase.fail as a signal handler"""
         self.fail_handler("A deadlock might be detected")
 
-def send_command(command_name, module_name, params=None, session=None, nonblock=False, timeout=None):
-    if session is not None:
-        cc_session = session
-    else:
-        cc_session = isc.cc.Session()
-    if timeout is not None:
-        orig_timeout = cc_session.get_timeout()
-        cc_session.set_timeout(timeout * 1000)
+def send_command(command_name, module_name, params=None):
+    cc_session = isc.cc.Session()
     command = isc.config.ccsession.create_command(command_name, params)
     seq = cc_session.group_sendmsg(command, module_name)
     try:
-        (answer, env) = cc_session.group_recvmsg(nonblock, seq)
+        (answer, env) = cc_session.group_recvmsg(False, seq)
         if answer:
             return isc.config.ccsession.parse_answer(answer)
     except isc.cc.SessionTimeout:
         pass
     finally:
-        if timeout is not None:
-            cc_session.set_timeout(orig_timeout)
-        if session is None:
-            cc_session.close()
-
-def send_shutdown(module_name, **kwargs):
-    return send_command("shutdown", module_name, **kwargs)
+        cc_session.close()
 
 class ThreadingServerManager:
     def __init__(self, server, *args, **kwargs):
@@ -467,6 +455,140 @@ class MockAuth:
             return isc.config.create_answer(0, sdata)
         return isc.config.create_answer(1, "Unknown Command")
 
+class MyModuleCCSession(isc.config.ConfigData):
+    """Mocked ModuleCCSession class.
+
+    This class incorporates the module spec directly from the file,
+    and works as if the ModuleCCSession class as much as possible
+    without involving network I/O.
+
+    """
+    def __init__(self, spec_file, config_handler, command_handler):
+        module_spec = isc.config.module_spec_from_file(spec_file)
+        isc.config.ConfigData.__init__(self, module_spec)
+        self._session = self
+        self.stopped = False
+        self.lname = 'mock_mod_ccs'
+
+    def start(self):
+        pass
+
+    def send_stopping(self):
+        self.stopped = True     # just record it's called to inspect it later
+
+class SimpleStats(stats.Stats):
+    """A faked Stats class for unit tests.
+
+    This class inherits most of the real Stats class, but replace the
+    ModuleCCSession with a fake one so we can avoid network I/O in tests,
+    and can also inspect or tweak messages via the session more easily.
+    This class also maintains some faked module information and statistics
+    data that can be retrieved from the implementation of the Stats class.
+
+    """
+    def __init__(self):
+        # First, setup some internal attributes.  All of them are essentially
+        # private (so prefixed with double '_'), but some are defined as if
+        # "protected" (with a single '_') for the convenient of tests that
+        # may want to inspect or tweak them.
+
+        # initial seq num for faked group_sendmsg, arbitrary choice.
+        self.__seq = 4200
+        # if set, use them as faked response to group_recvmsg (see below).
+        # it's a list of tuples, each of which is of (answer, envelope).
+        self._answers = []
+        # the default answer from faked recvmsg if _answers is empty
+        self.__default_answer = isc.config.ccsession.create_answer(
+            0, {'Init':
+                    json.loads(MockInit.spec_str)['module_spec']['statistics'],
+                'Auth':
+                    json.loads(MockAuth.spec_str)['module_spec']['statistics']
+                })
+        # setup faked auth statistics
+        self.__init_auth_stat()
+        # statistics data for faked Init module
+        self._init_sdata = {
+            'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
+            }
+
+        # Incorporate other setups of the real Stats module.  We use the faked
+        # ModuleCCSession to avoid blocking network operation.  Note also that
+        # we replace _init_statistics_data() (see below), so we don't
+        # initialize statistics data yet.
+        stats.Stats.__init__(self, MyModuleCCSession)
+
+        # replace some (faked) ModuleCCSession methods so we can inspect/fake
+        # the data exchanged via the CC session, then call
+        # _init_statistics_data.  This will get the Stats module info from
+        # the file directly and some amount information about the Init and
+        # Auth modules (hardcoded below).
+        self.cc_session.group_sendmsg = self.__group_sendmsg
+        self.cc_session.group_recvmsg = self.__group_recvmsg
+        self.cc_session.rpc_call = self.__rpc_call
+        stats.Stats._init_statistics_data(self)
+
+    def __init_auth_stat(self):
+        self._queries_tcp = 3
+        self._queries_udp = 2
+        self.__queries_per_zone = [{
+                'zonename': 'test1.example', 'queries.tcp': 5, 'queries.udp': 4
+                }]
+        self.__nds_queries_per_zone = \
+            { 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
+        self._auth_sdata = \
+            { 'queries.tcp': self._queries_tcp,
+              'queries.udp': self._queries_udp,
+              'queries.perzone' : self.__queries_per_zone,
+              'nds_queries.perzone' : {
+                'test10.example': {
+                    'queries.tcp': isc.cc.data.find(
+                        self.__nds_queries_per_zone,
+                        'test10.example/queries.tcp')
+                    }
+                },
+              'nds_queries.perzone/test10.example/queries.udp' :
+                  isc.cc.data.find(self.__nds_queries_per_zone,
+                                   'test10.example/queries.udp')
+              }
+
+    def _init_statistics_data(self):
+        # Inherited from real Stats class, just for deferring the
+        # initialization until we are ready.
+        pass
+
+    def __group_sendmsg(self, command, destination, want_answer=False):
+        """Faked ModuleCCSession.group_sendmsg for tests.
+
+        Skipping actual network communication, and just returning an internally
+        generated sequence number.
+
+        """
+        self.__seq += 1
+        return self.__seq
+
+    def __group_recvmsg(self, nonblocking, seq):
+        """Faked ModuleCCSession.group_recvmsg for tests.
+
+        Skipping actual network communication, and returning an internally
+        prepared answer. sequence number.  If faked anser is given in
+        _answers, use it; otherwise use the default.  we don't actually check
+        the sequence.
+
+        """
+        if len(self._answers) == 0:
+            return self.__default_answer, {'from': 'no-matter'}
+        return self._answers.pop(0)
+
+    def __rpc_call(self, command, group):
+        """Faked ModuleCCSession.rpc_call for tests.
+
+        At the moment we don't have to cover failure cases, so this is a
+        simple wrapper for the faked group_recvmsg().
+
+        """
+        answer, _ = self.__group_recvmsg(None, None)
+        return isc.config.ccsession.parse_answer(answer)[1]
+
 class MyStats(stats.Stats):
 
     stats._BASETIME = CONST_BASETIME



More information about the bind10-changes mailing list