Browse Source

[master] Merge branch 'trac2689'

JINMEI Tatuya 12 years ago
parent
commit
e606d8d0e6

+ 54 - 40
src/bin/stats/stats.py.in

@@ -190,12 +190,19 @@ class Stats:
     """
     """
     Main class of stats module
     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
         self.running = False
         # create ModuleCCSession object
         # 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
         self.cc_session = self.mccs._session
         # get module spec
         # get module spec
         self.module_name = self.mccs.get_module_spec().get_module_name()
         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
         # set a absolute timestamp polling at next time
         self.next_polltime = get_timestamp() + self.get_interval()
         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()
         self.update_modules()
         if self.update_statistics_data(
         if self.update_statistics_data(
             self.module_name,
             self.module_name,
@@ -316,11 +332,9 @@ class Stats:
         while len(sequences) > 0:
         while len(sequences) > 0:
             try:
             try:
                 (module_name, seq) = sequences.pop(0)
                 (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:
                 if answer:
-                    rcode, args = isc.config.ccsession.parse_answer(
-                        answer)
+                    rcode, args = isc.config.ccsession.parse_answer(answer)
                     if rcode == 0:
                     if rcode == 0:
                         _statistics_data.append(
                         _statistics_data.append(
                             (module_name, env['from'], args))
                             (module_name, env['from'], args))
@@ -347,31 +361,35 @@ class Stats:
         # if successfully done, set the last time of polling
         # if successfully done, set the last time of polling
         self._lasttime_poll = get_timestamp()
         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):
     def start(self):
         """
         """
         Start stats module
         Start stats module
         """
         """
         logger.info(STATS_STARTING)
         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:
         try:
             self.running = True
             self.running = True
             while self.running:
             while self.running:
-                _check_command()
+                self._check_command()
                 now = get_timestamp()
                 now = get_timestamp()
                 intval = self.get_interval()
                 intval = self.get_interval()
                 if intval > 0 and now >= self.next_polltime:
                 if intval > 0 and now >= self.next_polltime:
@@ -476,16 +494,16 @@ class Stats:
         updates statistics data. If specified data is invalid for
         updates statistics data. If specified data is invalid for
         statistics spec of specified owner, it returns a list of error
         statistics spec of specified owner, it returns a list of error
         messages. If there is no error or if neither owner nor data is
         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
         instance sends statistics data in the situation that multiple
         instances are working.
         instances are working.
         """
         """
         # Note:
         # Note:
         # The fix of #1751 is for multiple instances working. It is
         # The fix of #1751 is for multiple instances working. It is
         # assumed here that they send different statistics data with
         # 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
         # preserved by accumlating from statistics data by the mid. This
         # is an ad-hoc fix because administrators can not see
         # is an ad-hoc fix because administrators can not see
         # statistics by each instance via bindctl or HTTP/XML. These
         # statistics by each instance via bindctl or HTTP/XML. These
@@ -535,8 +553,7 @@ class Stats:
                             # merge recursively old value and new
                             # merge recursively old value and new
                             # value each other
                             # value each other
                             _data[owner][mid] = \
                             _data[owner][mid] = \
-                                merge_oldnew(_data[owner][mid],
-                                             {_key: _val})
+                                merge_oldnew(_data[owner][mid], {_key: _val})
                         continue
                         continue
                     # the key string might be a "xx/yy/zz[0]"
                     # the key string might be a "xx/yy/zz[0]"
                     # type. try it.
                     # type. try it.
@@ -546,22 +563,20 @@ class Stats:
                         if errors: errors.pop()
                         if errors: errors.pop()
                         # try updata and check validation in adavance
                         # try updata and check validation in adavance
                         __data = _data.copy()
                         __data = _data.copy()
-                        if owner not in _data:
+                        if owner not in __data:
                             __data[owner] = {}
                             __data[owner] = {}
-                        if mid not in _data[owner]:
+                        if mid not in __data[owner]:
                             __data[owner][mid] = {}
                             __data[owner][mid] = {}
                         # use the isc.cc.data.set method
                         # use the isc.cc.data.set method
                         try:
                         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(
                             if self.modules[owner].validate_statistics(
                                 False, __data[owner][mid], errors):
                                 False, __data[owner][mid], errors):
                                 _data = __data
                                 _data = __data
                         except Exception as e:
                         except Exception as e:
-                            errors.append(
-                                "%s: %s" % (e.__class__.__name__, e))
+                            errors.append("%s: %s" % (e.__class__.__name__, e))
             except KeyError:
             except KeyError:
-                errors.append("unknown module name: " + str(owner))
+                errors.append("unknown module name: " + owner)
             if not errors:
             if not errors:
                 self.statistics_data_bymid = _data
                 self.statistics_data_bymid = _data
 
 
@@ -584,8 +599,7 @@ class Stats:
                     # values are not replaced.
                     # values are not replaced.
                     self.statistics_data[m] = merge_oldnew(
                     self.statistics_data[m] = merge_oldnew(
                         self.statistics_data[m],
                         self.statistics_data[m],
-                        _accum_bymodule(
-                            self.statistics_data_bymid[m]))
+                        _accum_bymodule(self.statistics_data_bymid[m]))
 
 
         if errors: return errors
         if errors: return errors
 
 

+ 13 - 4
src/bin/stats/tests/b10-stats-httpd_test.py

@@ -48,7 +48,7 @@ import stats_httpd
 import stats
 import stats
 from test_utils import BaseModules, ThreadingServerManager, MyStats,\
 from test_utils import BaseModules, ThreadingServerManager, MyStats,\
                        MyStatsHttpd, SignalHandler,\
                        MyStatsHttpd, SignalHandler,\
-                       send_command, send_shutdown, CONST_BASETIME
+                       send_command, CONST_BASETIME
 from isc.testutils.ccsession_mock import MockModuleCCSession
 from isc.testutils.ccsession_mock import MockModuleCCSession
 
 
 # This test suite uses xml.etree.ElementTree.XMLParser via
 # 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()) + ")"))
                          (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
         # failure case(Stats is down)
         # failure case(Stats is down)
         self.assertTrue(self.stats.running)
         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.assertFalse(self.stats.running)
         self.stats_httpd.cc_session.set_timeout(milliseconds=100)
         self.stats_httpd.cc_session.set_timeout(milliseconds=100)
 
 
@@ -608,8 +609,16 @@ class TestStatsHttpd(unittest.TestCase):
         self.stats_server.run()
         self.stats_server.run()
         # checking IPv6 enabled on this platform
         # checking IPv6 enabled on this platform
         self.ipv6_enabled = is_ipv6_enabled()
         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):
     def tearDown(self):
+        socket.gethostbyaddr = self.__gethostbyaddr_orig
         if hasattr(self, "stats_httpd"):
         if hasattr(self, "stats_httpd"):
             self.stats_httpd.stop()
             self.stats_httpd.stop()
         self.stats_server.shutdown()
         self.stats_server.shutdown()
@@ -751,7 +760,7 @@ class TestStatsHttpd(unittest.TestCase):
         self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, server_addresses)
         self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, server_addresses)
         self.stats_httpd_server.run()
         self.stats_httpd_server.run()
         self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, server_addresses)
         self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, server_addresses)
-        send_shutdown("StatsHttpd")
+        send_command("shutdown", "StatsHttpd")
 
 
     def test_running(self):
     def test_running(self):
         self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, get_availaddr())
         self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, get_availaddr())
@@ -761,7 +770,7 @@ class TestStatsHttpd(unittest.TestCase):
         self.assertEqual(send_command("status", "StatsHttpd"),
         self.assertEqual(send_command("status", "StatsHttpd"),
                          (0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
                          (0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
         self.assertTrue(self.stats_httpd.running)
         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.assertFalse(self.stats_httpd.running)
         self.stats_httpd_server.shutdown()
         self.stats_httpd_server.shutdown()
 
 

+ 272 - 157
src/bin/stats/tests/b10-stats_test.py

@@ -32,7 +32,8 @@ import sys
 import stats
 import stats
 import isc.log
 import isc.log
 import isc.cc.session
 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
 from isc.testutils.ccsession_mock import MockModuleCCSession
 
 
 class TestUtilties(unittest.TestCase):
 class TestUtilties(unittest.TestCase):
@@ -247,11 +248,17 @@ class TestStats(unittest.TestCase):
         self.const_timestamp = 1308730448.965706
         self.const_timestamp = 1308730448.965706
         self.const_datetime = '2011-06-22T08:14:08Z'
         self.const_datetime = '2011-06-22T08:14:08Z'
         self.const_default_datetime = '1970-01-01T00:00:00Z'
         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):
     def tearDown(self):
         self.base.shutdown()
         self.base.shutdown()
         # reset the signal handler
         # reset the signal handler
         self.sig_handler.reset()
         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):
     def test_init(self):
         self.stats = stats.Stats()
         self.stats = stats.Stats()
@@ -287,133 +294,212 @@ class TestStats(unittest.TestCase):
         self.assertRaises(stats.StatsError, stats.Stats)
         self.assertRaises(stats.StatsError, stats.Stats)
         stats.SPECFILE_LOCATION = orig_spec_location
         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):
     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
         # 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):
     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(
         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}}))
             (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(
         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}}))
             (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(
         self.assertEqual(
-            send_command('status', 'Stats'),
+            self.__send_command(__stats, 'status'),
             (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
             (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(
         self.assertEqual(
-            send_command('__UNKNOWN__', 'Stats'),
+            self.__send_command(__stats, '__UNKNOWN__'),
             (1, "Unknown command: '__UNKNOWN__'"))
             (1, "Unknown command: '__UNKNOWN__'"))
 
 
-        self.stats_server.shutdown()
-
     def test_update_modules(self):
     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()
         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('Stats' in self.stats.modules)
         self.assertTrue('Init' in self.stats.modules)
         self.assertTrue('Init' in self.stats.modules)
         self.assertFalse('Dummy' 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('report_time' in my_statistics_data)
         self.assertTrue('boot_time' in my_statistics_data)
         self.assertTrue('boot_time' in my_statistics_data)
         self.assertTrue('last_update_time' in my_statistics_data)
         self.assertTrue('last_update_time' in my_statistics_data)
         self.assertTrue('timestamp' in my_statistics_data)
         self.assertTrue('timestamp' in my_statistics_data)
         self.assertTrue('lname' 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['timestamp'], 0.0)
         self.assertEqual(my_statistics_data['lname'], "")
         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.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
         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)
         self.assertRaises(stats.StatsError, self.stats.update_modules)
-        stats.isc.config.ccsession.parse_answer = orig_parse_answer
 
 
     def test_get_statistics_data(self):
     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()
         my_statistics_data = self.stats.get_statistics_data()
         self.assertTrue('Stats' in my_statistics_data)
         self.assertTrue('Stats' in my_statistics_data)
         self.assertTrue('Init' in my_statistics_data)
         self.assertTrue('Init' in my_statistics_data)
         self.assertTrue('boot_time' in my_statistics_data['Init'])
         self.assertTrue('boot_time' in my_statistics_data['Init'])
+
         my_statistics_data = self.stats.get_statistics_data(owner='Stats')
         my_statistics_data = self.stats.get_statistics_data(owner='Stats')
         self.assertTrue('Stats' in my_statistics_data)
         self.assertTrue('Stats' in my_statistics_data)
         self.assertTrue('report_time' in my_statistics_data['Stats'])
         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('last_update_time' in my_statistics_data['Stats'])
         self.assertTrue('timestamp' in my_statistics_data['Stats'])
         self.assertTrue('timestamp' in my_statistics_data['Stats'])
         self.assertTrue('lname' 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'])
         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'])
         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)
         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.assertTrue(len(my_statistics_data['Stats']['lname']) >0)
         self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
         self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
                           owner='Stats', name='Bar')
                           owner='Stats', name='Bar')
@@ -441,7 +539,7 @@ class TestStats(unittest.TestCase):
 
 
     def test_update_statistics_data(self):
     def test_update_statistics_data(self):
         """test for list-type statistics"""
         """test for list-type statistics"""
-        self.stats = stats.Stats()
+        self.stats = SimpleStats()
         _test_exp1 = {
         _test_exp1 = {
               'zonename': 'test1.example',
               'zonename': 'test1.example',
               'queries.tcp': 5,
               'queries.tcp': 5,
@@ -518,42 +616,20 @@ class TestStats(unittest.TestCase):
 
 
     def test_update_statistics_data_pt2(self):
     def test_update_statistics_data_pt2(self):
         """test for named_set-type statistics"""
         """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_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 ={
         _test_exp5_2 ={
               'nds_queries.perzone/test10.example/queries.udp':
               '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
         # Success cases
         self.assertIsNone(self.stats.update_statistics_data(
         self.assertIsNone(self.stats.update_statistics_data(
             'Auth', 'foo1', {'nds_queries.perzone': _test_exp1}))
             'Auth', 'foo1', {'nds_queries.perzone': _test_exp1}))
@@ -566,13 +642,15 @@ class TestStats(unittest.TestCase):
                              ['foo1']['nds_queries.perzone'],\
                              ['foo1']['nds_queries.perzone'],\
                          dict(_test_exp1,**_test_exp2))
                          dict(_test_exp1,**_test_exp2))
         self.assertIsNone(self.stats.update_statistics_data(
         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']\
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
                              ['foo1']['nds_queries.perzone'],
                              ['foo1']['nds_queries.perzone'],
-                         dict(_test_exp1,**_test_exp2))
+                         dict(_test_exp1, **_test_exp2))
         # differential update
         # differential update
         self.assertIsNone(self.stats.update_statistics_data(
         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,
         _new_val = dict(_test_exp1,
                         **stats.merge_oldnew(_test_exp2,_test_exp4))
                         **stats.merge_oldnew(_test_exp2,_test_exp4))
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
@@ -592,7 +670,8 @@ class TestStats(unittest.TestCase):
                              _test_exp5_1)
                              _test_exp5_1)
         # Error cases
         # Error cases
         self.assertEqual(self.stats.update_statistics_data(
         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']\
         self.assertEqual(self.stats.statistics_data_bymid['Auth']\
                              ['foo1']['nds_queries.perzone'],\
                              ['foo1']['nds_queries.perzone'],\
                              _new_val)
                              _new_val)
@@ -606,33 +685,57 @@ class TestStats(unittest.TestCase):
         self.assertEqual(self.stats.update_statistics_data(
         self.assertEqual(self.stats.update_statistics_data(
                 'Foo', 'foo1', _test_exp6), ['unknown module name: Foo'])
                 '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):
     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()
         self.stats.do_polling()
+
         # samples of query number
         # samples of query number
         bar1_tcp = 1001
         bar1_tcp = 1001
         bar2_tcp = 2001
         bar2_tcp = 2001
         bar3_tcp = 1002
         bar3_tcp = 1002
         bar3_udp = 1003
         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@foo",
         self.stats.update_statistics_data('Auth', "bar1@foo",
-                                          {'queries.tcp':bar1_tcp})
+                                          {'queries.tcp': bar1_tcp})
         self.assertTrue('Auth' in self.stats.statistics_data)
         self.assertTrue('Auth' in self.stats.statistics_data)
         self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
         self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
         self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
         self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
                          bar1_tcp + sum_qtcp)
                          bar1_tcp + sum_qtcp)
         self.assertTrue('Auth' in self.stats.statistics_data_bymid)
         self.assertTrue('Auth' in self.stats.statistics_data_bymid)
         self.assertTrue('bar1@foo' in self.stats.statistics_data_bymid['Auth'])
         self.assertTrue('bar1@foo' in self.stats.statistics_data_bymid['Auth'])
-        self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid['Auth']['bar1@foo'])
+        self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid
+                        ['Auth']['bar1@foo'])
         self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar1@foo'],
         self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar1@foo'],
                          {'queries.tcp': bar1_tcp})
                          {'queries.tcp': bar1_tcp})
         # check consolidation of statistics data even if there is
         # 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.assertTrue('queries.udp' in self.stats.statistics_data['Auth'])
         self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
         self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
                          bar1_tcp + bar2_tcp + sum_qtcp)
                          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)
         self.assertTrue('Auth' in self.stats.statistics_data_bymid)
         # restore statistics data of killed auth
         # restore statistics data of killed auth
         # self.base.b10_init.server.pid_list = [ killed ] + self.base.b10_init.server.pid_list[:]
         # 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):
     def test_config(self):
         orig_get_timestamp = stats.get_timestamp
         orig_get_timestamp = stats.get_timestamp
         stats.get_timestamp = lambda : self.const_timestamp
         stats.get_timestamp = lambda : self.const_timestamp
-        stats_server = ThreadingServerManager(MyStats)
-        stat = stats_server.server
+        stat = SimpleStats()
+
         # test updating poll-interval
         # test updating poll-interval
         self.assertEqual(stat.config['poll-interval'], 60)
         self.assertEqual(stat.config['poll-interval'], 60)
         self.assertEqual(stat.get_interval(), 60)
         self.assertEqual(stat.get_interval(), 60)
@@ -715,14 +819,25 @@ class TestStats(unittest.TestCase):
         self.assertEqual(stat.config_handler({'poll-interval': 0}),
         self.assertEqual(stat.config_handler({'poll-interval': 0}),
                          isc.config.create_answer(0))
                          isc.config.create_answer(0))
         self.assertEqual(stat.config['poll-interval'], 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(
         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}}))
             (0, {'Init': {'boot_time': self.const_datetime}}))
-        stats_server.shutdown()
 
 
     def test_commands(self):
     def test_commands(self):
         self.stats = stats.Stats()
         self.stats = stats.Stats()

+ 138 - 16
src/bin/stats/tests/test_utils.py

@@ -51,30 +51,18 @@ class SignalHandler():
         """envokes unittest.TestCase.fail as a signal handler"""
         """envokes unittest.TestCase.fail as a signal handler"""
         self.fail_handler("A deadlock might be detected")
         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)
     command = isc.config.ccsession.create_command(command_name, params)
     seq = cc_session.group_sendmsg(command, module_name)
     seq = cc_session.group_sendmsg(command, module_name)
     try:
     try:
-        (answer, env) = cc_session.group_recvmsg(nonblock, seq)
+        (answer, env) = cc_session.group_recvmsg(False, seq)
         if answer:
         if answer:
             return isc.config.ccsession.parse_answer(answer)
             return isc.config.ccsession.parse_answer(answer)
     except isc.cc.SessionTimeout:
     except isc.cc.SessionTimeout:
         pass
         pass
     finally:
     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:
 class ThreadingServerManager:
     def __init__(self, server, *args, **kwargs):
     def __init__(self, server, *args, **kwargs):
@@ -467,6 +455,140 @@ class MockAuth:
             return isc.config.create_answer(0, sdata)
             return isc.config.create_answer(0, sdata)
         return isc.config.create_answer(1, "Unknown Command")
         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):
 class MyStats(stats.Stats):
 
 
     stats._BASETIME = CONST_BASETIME
     stats._BASETIME = CONST_BASETIME