Commit 8fbdef45 authored by Naoki Kambe's avatar Naoki Kambe
Browse files

[master] Merge branch 'trac2781'

parents 442c5bd6 3669c959
#!@PYTHON@ #!@PYTHON@
# Copyright (C) 2010, 2011, 2012 Internet Systems Consortium. # Copyright (C) 2010-2013 Internet Systems Consortium.
# #
# Permission to use, copy, modify, and distribute this software for any # Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above # purpose with or without fee is hereby granted, provided that the above
...@@ -124,8 +124,8 @@ def _accum(a, b): ...@@ -124,8 +124,8 @@ def _accum(a, b):
if len(a) <= i ] if len(a) <= i ]
# If both of args are integer or float type, two # If both of args are integer or float type, two
# values are added. # values are added.
elif (type(a) is int and type(b) is int) \ elif (type(a) is int or type(a) is float) \
or (type(a) is float or type(b) is float): and (type(b) is int or type(b) is float):
return a + b return a + b
# If both of args are string type, # If both of args are string type,
...@@ -186,6 +186,11 @@ class StatsError(Exception): ...@@ -186,6 +186,11 @@ class StatsError(Exception):
"""Exception class for Stats class""" """Exception class for Stats class"""
pass pass
class InitSessionTimeout(isc.cc.session.SessionTimeout):
"""SessionTimeout Exception in the session between the stats module and the
init module"""
pass
class Stats: class Stats:
""" """
Main class of stats module Main class of stats module
...@@ -243,14 +248,12 @@ class Stats: ...@@ -243,14 +248,12 @@ class Stats:
""" """
self.update_modules() self.update_modules()
if self.update_statistics_data( self.update_statistics_data(
self.module_name, self.module_name,
self.cc_session.lname, self.cc_session.lname,
{'lname': self.cc_session.lname, {'lname': self.cc_session.lname,
'boot_time': get_datetime(_BASETIME), 'boot_time': get_datetime(_BASETIME),
'last_update_time': get_datetime()}): 'last_update_time': get_datetime()})
logger.warn(STATS_RECEIVED_INVALID_STATISTICS_DATA,
self.module_name)
# define the variable of the last time of polling # define the variable of the last time of polling
self._lasttime_poll = 0.0 self._lasttime_poll = 0.0
...@@ -258,12 +261,8 @@ class Stats: ...@@ -258,12 +261,8 @@ class Stats:
"""return the current value of 'poll-interval'""" """return the current value of 'poll-interval'"""
return self.config['poll-interval'] return self.config['poll-interval']
def do_polling(self): def _get_multi_module_list(self):
"""Polls modules for statistics data. Return nothing. First """Returns a module list which is running as multiple modules."""
search multiple instances of same module. Second requests
each module to invoke 'getstats'. Finally updates internal
statistics data every time it gets from each instance."""
# It counts the number of instances of same module by # It counts the number of instances of same module by
# examining the third value from the array result of # examining the third value from the array result of
# 'show_processes' of Init # 'show_processes' of Init
...@@ -277,6 +276,8 @@ class Stats: ...@@ -277,6 +276,8 @@ class Stats:
# TODO: Is it OK to just pass? As part of refactoring, preserving # TODO: Is it OK to just pass? As part of refactoring, preserving
# the original behaviour. # the original behaviour.
value = None value = None
except isc.cc.session.SessionTimeout as e:
raise InitSessionTimeout(e)
modules = [] modules = []
if type(value) is list: if type(value) is list:
# NOTE: For example, the "show_processes" command # NOTE: For example, the "show_processes" command
...@@ -306,6 +307,12 @@ class Stats: ...@@ -306,6 +307,12 @@ class Stats:
# release. # release.
modules = [ v[2] if type(v) is list and len(v) > 2 \ modules = [ v[2] if type(v) is list and len(v) > 2 \
else None for v in value ] else None for v in value ]
return modules
def _query_statistics(self, modules):
"""Queries each module statistics and returns sequences to use
for receiving that answers based on the argument 'modules' which
is a list of modules running as multiple modules"""
# start requesting each module to collect statistics data # start requesting each module to collect statistics data
sequences = [] sequences = []
for (module_name, data) in self.get_statistics_data().items(): for (module_name, data) in self.get_statistics_data().items():
...@@ -327,11 +334,17 @@ class Stats: ...@@ -327,11 +334,17 @@ class Stats:
if cnt > 1: if cnt > 1:
sequences = sequences + [ (module_name, seq) \ sequences = sequences + [ (module_name, seq) \
for i in range(cnt-1) ] for i in range(cnt-1) ]
return sequences
def _collect_statistics(self, sequences):
"""Based on sequences which is a list of values returned from
group_sendmsg(), collects statistics data from each module. If
a SessionTimeout exception is raised when collecting from the
module, skips it and goes to collect from the next module."""
# start receiving statistics data # start receiving statistics data
_statistics_data = [] _statistics_data = []
while len(sequences) > 0: for (module_name, seq) in sequences:
try: 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: if answer:
rcode, args = isc.config.ccsession.parse_answer(answer) rcode, args = isc.config.ccsession.parse_answer(answer)
...@@ -340,25 +353,37 @@ class Stats: ...@@ -340,25 +353,37 @@ class Stats:
(module_name, env['from'], args)) (module_name, env['from'], args))
# skip this module if SessionTimeout raised # skip this module if SessionTimeout raised
except isc.cc.session.SessionTimeout: except isc.cc.session.SessionTimeout:
pass logger.warn(STATS_SKIP_COLLECTING, module_name)
return _statistics_data
def _refresh_statistics(self, statistics_data):
"""Refreshes statistics_data into internal statistics data to
display, which is a list of a tuple of module_name, lname, and
args"""
# update statistics data # update statistics data
_statistics_data = statistics_data[:]
self.update_modules() self.update_modules()
while len(_statistics_data) > 0: while len(_statistics_data) > 0:
(_module_name, _lname, _args) = _statistics_data.pop(0) (_module_name, _lname, _args) = _statistics_data.pop(0)
if self.update_statistics_data(_module_name, _lname, _args): if not self.update_statistics_data(_module_name, _lname, _args):
logger.warn( self.update_statistics_data(
STATS_RECEIVED_INVALID_STATISTICS_DATA,
_module_name)
else:
if self.update_statistics_data(
self.module_name, self.module_name,
self.cc_session.lname, self.cc_session.lname,
{'last_update_time': get_datetime()}): {'last_update_time': get_datetime()})
logger.warn(
STATS_RECEIVED_INVALID_STATISTICS_DATA, def do_polling(self):
self.module_name) """Polls modules for statistics data. Return nothing. First
# if successfully done, set the last time of polling search multiple instances of same module. Second requests
each module to invoke 'getstats'. Finally updates internal
statistics data every time it gets from each instance."""
try:
modules = self._get_multi_module_list()
sequences = self._query_statistics(modules)
_statistics_data = self._collect_statistics(sequences)
self._refresh_statistics(_statistics_data)
except InitSessionTimeout:
logger.warn(STATS_SKIP_POLLING)
# if successfully done or skipped, set the last time of polling
self._lasttime_poll = get_timestamp() self._lasttime_poll = get_timestamp()
def _check_command(self, nonblock=False): def _check_command(self, nonblock=False):
...@@ -601,7 +626,10 @@ class Stats: ...@@ -601,7 +626,10 @@ class Stats:
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:
logger.warn(
STATS_RECEIVED_INVALID_STATISTICS_DATA, owner)
return errors
def command_status(self): def command_status(self):
""" """
......
...@@ -64,6 +64,17 @@ will respond with an error and the command will be ignored. ...@@ -64,6 +64,17 @@ will respond with an error and the command will be ignored.
This debug message is printed when a request is sent to the module This debug message is printed when a request is sent to the module
to send its data to the stats module. to send its data to the stats module.
% STATS_SKIP_COLLECTING skipped collecting statistics from %1
The stats module temporarily encountered an internal messaging bus error while
collecting statistics from the module, then it skipped collecting statistics
from it and will try to collect from the next module. The lack of statistics
will be recovered at the next polling round.
% STATS_SKIP_POLLING skipped polling statistics to modules
The stats module temporarily encountered an internal messaging bus error while
collecting initial information to collect statistics from the init module, then
it skipped polling statistics and will try to do next time.
% STATS_STARTING starting % STATS_STARTING starting
The stats module will be now starting. The stats module will be now starting.
......
# Copyright (C) 2010, 2011, 2012 Internet Systems Consortium. # Copyright (C) 2010-2013 Internet Systems Consortium.
# #
# Permission to use, copy, modify, and distribute this software for any # Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above # purpose with or without fee is hereby granted, provided that the above
...@@ -31,6 +31,7 @@ import sys ...@@ -31,6 +31,7 @@ import sys
import stats import stats
import isc.log import isc.log
from test_utils import MyStats from test_utils import MyStats
from isc.config.ccsession import create_answer
class TestUtilties(unittest.TestCase): class TestUtilties(unittest.TestCase):
items = [ items = [
...@@ -132,6 +133,8 @@ class TestUtilties(unittest.TestCase): ...@@ -132,6 +133,8 @@ class TestUtilties(unittest.TestCase):
self.assertEqual(stats._accum("a", None), "a") self.assertEqual(stats._accum("a", None), "a")
self.assertEqual(stats._accum(1, 2), 3) self.assertEqual(stats._accum(1, 2), 3)
self.assertEqual(stats._accum(0.5, 0.3), 0.8) self.assertEqual(stats._accum(0.5, 0.3), 0.8)
self.assertEqual(stats._accum(1, 0.3), 1.3)
self.assertEqual(stats._accum(0.5, 2), 2.5)
self.assertEqual(stats._accum('aa','bb'), 'bb') self.assertEqual(stats._accum('aa','bb'), 'bb')
self.assertEqual(stats._accum('1970-01-01T09:00:00Z','2012-08-09T09:33:31Z'), self.assertEqual(stats._accum('1970-01-01T09:00:00Z','2012-08-09T09:33:31Z'),
'2012-08-09T09:33:31Z') '2012-08-09T09:33:31Z')
...@@ -296,6 +299,43 @@ class TestStats(unittest.TestCase): ...@@ -296,6 +299,43 @@ class TestStats(unittest.TestCase):
return isc.config.ccsession.parse_answer( return isc.config.ccsession.parse_answer(
stats.command_handler(command_name, params)) stats.command_handler(command_name, params))
def test_check_command(self):
"""Test _check_command sets proper timeout values and sets proper values
returned from group_recvmsg"""
stat = MyStats()
set_timeouts = []
orig_timeout = 1
stat.cc_session.get_timeout = lambda: orig_timeout
stat.cc_session.set_timeout = lambda x: set_timeouts.append(x)
msg = create_answer(0, 'msg')
env = {'from': 'frominit'}
stat._answers = [(msg, env)]
stat._check_command()
self.assertListEqual([500, orig_timeout], set_timeouts)
self.assertEqual(msg, stat.mccs._msg)
self.assertEqual(env, stat.mccs._env)
def test_check_command_sessiontimeout(self):
"""Test _check_command doesn't perform but sets proper timeout values in
case that a SesstionTimeout exception is caught while doing
group_recvmsg()"""
stat = MyStats()
set_timeouts = []
orig_timeout = 1
stat.cc_session.get_timeout = lambda: orig_timeout
stat.cc_session.set_timeout = lambda x: set_timeouts.append(x)
msg = create_answer(0, 'msg')
env = {'from': 'frominit'}
stat._answers = [(msg, env)]
# SessionTimeout is raised while doing group_recvmsg()
ex = isc.cc.session.SessionTimeout
def __raise(*x): raise ex(*x)
stat.cc_session.group_recvmsg = lambda x: __raise()
stat._check_command()
self.assertListEqual([500, orig_timeout], set_timeouts)
self.assertEqual(None, stat.mccs._msg)
self.assertEqual(None, stat.mccs._env)
def test_start(self): def test_start(self):
# Define a separate exception class so we can be sure that's actually # Define a separate exception class so we can be sure that's actually
# the one raised in __check_start() below # the one raised in __check_start() below
...@@ -315,6 +355,25 @@ class TestStats(unittest.TestCase): ...@@ -315,6 +355,25 @@ class TestStats(unittest.TestCase):
self.assertRaises(CheckException, self.stats.start) self.assertRaises(CheckException, self.stats.start)
self.assertEqual(self.__send_command(self.stats, "status"), self.assertEqual(self.__send_command(self.stats, "status"),
(0, "Stats is up. (PID " + str(os.getpid()) + ")")) (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
self.assertTrue(self.stats.mccs.stopped)
def test_start_set_next_polltime(self):
"""Test start() properly sets the time next_polltime to do_poll() next
time"""
orig_get_timestamp = stats.get_timestamp
stats.get_timestamp = lambda : self.const_timestamp
stat = MyStats()
# manupilate next_polltime to go it through the inner if-condition
stat.next_polltime = self.const_timestamp - stat.get_interval() - 1
# stop an infinity loop at once
def __stop_running(): stat.running = False
stat.do_polling = __stop_running
# do nothing in _check_command()
stat._check_command = lambda: None
stat.start()
# check stat.next_polltime reassigned
self.assertEqual(self.const_timestamp, stat.next_polltime)
stats.get_timestamp = orig_get_timestamp
def test_shutdown(self): def test_shutdown(self):
def __check_shutdown(tested_stats): def __check_shutdown(tested_stats):
...@@ -697,7 +756,6 @@ class TestStats(unittest.TestCase): ...@@ -697,7 +756,6 @@ class TestStats(unittest.TestCase):
# We use the knowledge of what kind of messages are sent via # We use the knowledge of what kind of messages are sent via
# do_polling, and return the following faked answer directly. # do_polling, and return the following faked answer directly.
create_answer = isc.config.ccsession.create_answer # shortcut
self.stats._answers = [ self.stats._answers = [
# Answer for "show_processes" # Answer for "show_processes"
(create_answer(0, [[1034, 'b10-auth-1', 'Auth'], (create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
...@@ -817,7 +875,6 @@ class TestStats(unittest.TestCase): ...@@ -817,7 +875,6 @@ class TestStats(unittest.TestCase):
# see the comment for test_update_statistics_data_withmid. We abuse # see the comment for test_update_statistics_data_withmid. We abuse
# do_polling here, too. With #2781 we should make it more direct. # do_polling here, too. With #2781 we should make it more direct.
create_answer = isc.config.ccsession.create_answer # shortcut
stat._answers = [\ stat._answers = [\
# Answer for "show_processes" # Answer for "show_processes"
(create_answer(0, []), None), (create_answer(0, []), None),
...@@ -868,7 +925,6 @@ class TestStats(unittest.TestCase): ...@@ -868,7 +925,6 @@ class TestStats(unittest.TestCase):
self.stats.update_modules = lambda: None self.stats.update_modules = lambda: None
# Test data borrowed from test_update_statistics_data_withmid # Test data borrowed from test_update_statistics_data_withmid
create_answer = isc.config.ccsession.create_answer # shortcut
self.stats._answers = [ self.stats._answers = [
(create_answer(0, [[1034, 'b10-auth-1', 'Auth'], (create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
[1035, 'b10-auth-2', 'Auth']]), None), [1035, 'b10-auth-2', 'Auth']]), None),
...@@ -1281,6 +1337,163 @@ class TestStats(unittest.TestCase): ...@@ -1281,6 +1337,163 @@ class TestStats(unittest.TestCase):
isc.config.create_answer( isc.config.create_answer(
1, "module name is not specified")) 1, "module name is not specified"))
def test_get_multi_module_list(self):
"""Test _get_multi_module_list() returns a module list which is running
as multiple modules."""
stat = MyStats()
# no answer
self.assertListEqual([], stat._get_multi_module_list())
# proc list returned
proc_list = [
[29317, 'b10-xfrout', 'Xfrout'],
[29318, 'b10-xfrin', 'Xfrin'],
[20061, 'b10-auth','Auth'],
[20103, 'b10-auth-2', 'Auth']]
mod_list = [ a[2] for a in proc_list ]
stat._answers = [
# Answer for "show_processes"
(create_answer(0, proc_list), {'from': 'init'})
]
self.assertListEqual(mod_list, stat._get_multi_module_list())
# invalid proc list
stat._answers = [
# Answer for "show_processes"
(create_answer(0, [[999, 'invalid', 'Invalid'], 'invalid']),
{'from': 'init'})
]
self.assertListEqual(['Invalid', None], stat._get_multi_module_list())
def test_get_multi_module_list_rpcrecipientmissing(self):
"""Test _get_multi_module_list() raises an RPCRecipientMissing exception
if rcp_call() raise the exception"""
# RPCRecipientMissing case
stat = MyStats()
ex = isc.config.RPCRecipientMissing
def __raise(*x): raise ex(*x)
stat.mccs.rpc_call = lambda x,y: __raise('Error')
self.assertRaises(ex, stat._get_multi_module_list)
def test_get_multi_module_list_rpcerror(self):
"""Test _get_multi_module_list() returns an empty list if rcp_call()
raise an RPCError exception"""
# RPCError case
stat = MyStats()
ex = isc.config.RPCError
def __raise(*x): raise ex(*x)
stat.mccs.rpc_call = lambda x,y: __raise(99, 'Error')
self.assertListEqual([], stat._get_multi_module_list())
def test_get_multi_module_list_initsessiontimeout(self):
"""Test _get_multi_module_list() raises an InitSeeionTimeout exception
if a CC session times out in rcp_call()"""
# InitSeeionTimeout case
stat = MyStats()
ex = isc.cc.session.SessionTimeout
def __raise(*x): raise ex(*x)
stat.mccs.rpc_call = lambda x,y: __raise()
self.assertRaises(stats.InitSessionTimeout, stat._get_multi_module_list)
def test_query_statistics(self):
"""Test _query_statistics returns a list of pairs of module and
sequences from group_sendmsg()"""
stat = MyStats()
# imitate stat.get_statistics_data().items. The order of the array
# returned by items() should be preserved.
class DummyDict:
def items(self):
return [('Init', 'dummy'), ('Stats', 'dummy'), ('Auth', 'dummy')]
stat.get_statistics_data = lambda: DummyDict()
mod = ('Init', 'Auth', 'Auth')
seq = [('Init', stat._seq + 1),
('Auth', stat._seq + 2),
('Auth', stat._seq + 2) ]
self.assertListEqual(seq, stat._query_statistics(mod))
def test_collect_statistics(self):
"""Test _collect_statistics() collects statistics data from each module
based on the sequences which is a list of values returned from
group_sendmsg()"""
stat = MyStats()
seq = [ ('Init', stat._seq + 1),
('Auth', stat._seq + 2),
('Auth', stat._seq + 2) ]
ret = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'}),
('Auth', 'fromauth1', {'queries.tcp': 100}),
('Auth', 'fromauth2', {'queries.udp': 200})]
stat._answers = [
(create_answer(0, r[2]), {'from': r[1]}) for r in ret
]
self.assertListEqual(ret, stat._collect_statistics(seq))
def test_collect_statistics_nodata(self):
"""Test _collect_statistics() returns empty statistics data if
a module returns an empty list"""
stat = MyStats()
seq = []
stat._answers = []
ret = []
self.assertListEqual(ret, stat._collect_statistics(seq))
def test_collect_statistics_nonzero_rcode(self):
"""Test _collect_statistics() returns empty statistics data if
a module returns non-zero rcode"""
stat = MyStats()
seq = [('Init', stat._seq + 1)]
stat._answers = [
(create_answer(1, 'error'), {'from': 'frominit'})
]
ret = []
self.assertListEqual(ret, stat._collect_statistics(seq))
def test_collect_statistics_sessiontimeout(self):
"""Test _collect_statistics() collects statistics data from each module
based on the sequences which is a list of values returned from
group_sendmsg(). In this test case, SessionTimeout exceptions are raised
while collecting from Auth. This tests _collect_statistics skips
collecting from Auth."""
# SessionTimeout case
stat = MyStats()
ex = isc.cc.session.SessionTimeout
def __raise(*x): raise ex(*x)
# SessionTimeout is raised when asking to Auth
stat.cc_session.group_recvmsg = lambda x,seq: \
__raise() if seq == stat._seq + 2 else stat._answers.pop(0)
seq = [ ('Init', stat._seq + 1),
('Auth', stat._seq + 2),
('Auth', stat._seq + 2) ]
ret = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'})]
stat._answers = [
(create_answer(0, r[2]), {'from': r[1]}) for r in ret
]
self.assertListEqual(ret, stat._collect_statistics(seq))
def test_refresh_statistics(self):
"""Test _refresh_statistics() refreshes statistics data from given data
"""
stat = MyStats()
self.assertEqual(self.const_default_datetime,
stat.statistics_data['Init']['boot_time'])
self.assertEqual(0,
stat.statistics_data['Auth']['queries.tcp'])
self.assertEqual(0,
stat.statistics_data['Auth']['queries.udp'])
# change stats.get_datetime() for testing 'last_update_time'
orig_get_datetime = stats.get_datetime
stats.get_datetime = lambda : self.const_datetime
arg = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'}),
('Auth', 'fromauth1', {'queries.tcp': 100}),
('Auth', 'fromauth2', {'queries.udp': 200})]
stat._refresh_statistics(arg)
self.assertEqual('2013-01-01T00:00:00Z',
stat.statistics_data['Init']['boot_time'])
self.assertEqual(100,
stat.statistics_data['Auth']['queries.tcp'])
self.assertEqual(200,
stat.statistics_data['Auth']['queries.udp'])
self.assertEqual(self.const_datetime,
stat.statistics_data['Stats']['last_update_time'])
stats.get_datetime = orig_get_datetime
def test_polling_init(self): def test_polling_init(self):
"""check statistics data of 'Init'.""" """check statistics data of 'Init'."""
...@@ -1295,7 +1508,6 @@ class TestStats(unittest.TestCase): ...@@ -1295,7 +1508,6 @@ class TestStats(unittest.TestCase):
del stat.statistics_data['Auth'] del stat.statistics_data['Auth']
stat.update_modules = lambda: None stat.update_modules = lambda: None
create_answer = isc.config.ccsession.create_answer # shortcut
stat._answers = [ stat._answers = [
# Answer for "show_processes" # Answer for "show_processes"
...@@ -1314,7 +1526,6 @@ class TestStats(unittest.TestCase): ...@@ -1314,7 +1526,6 @@ class TestStats(unittest.TestCase):
"""check statistics data of multiple instances of same module.""" """check statistics data of multiple instances of same module."""
stat = MyStats() stat = MyStats()
stat.update_modules = lambda: None stat.update_modules = lambda: None
create_answer = isc.config.ccsession.create_answer # shortcut
# Test data borrowed from test_update_statistics_data_withmid # Test data borrowed from test_update_statistics_data_withmid
stat._answers = [ stat._answers = [
...@@ -1380,35 +1591,52 @@ class TestStats(unittest.TestCase): ...@@ -1380,35 +1591,52 @@ class TestStats(unittest.TestCase):
self.assertEqual(stat.statistics_data['Stats']['lname'], self.assertEqual(stat.statistics_data['Stats']['lname'],
stat.mccs._session.lname) stat.mccs._session.lname)
def test_polling2(self): def test_refresh_statistics_broken_statistics_data(self):
"""Test do_polling() doesn't incorporate broken statistics data. """Test _refresh_statistics() doesn't incorporate broken statistics data
Actually, this is not a test for do_polling() itself. It's bad, but
fixing that is a subject of different ticket.
""" """
stat = MyStats() stat = MyStats()
# check default statistics data of 'Init' # check default statistics data of 'Init'
self.assertEqual( self.assertEqual(
stat.statistics_data['Init'], {'boot_time': self.const_default_datetime},
{'boot_time': self.const_default_datetime}) stat.statistics_data['Init'])
last_update_time = stat.statistics_data['Stats']['last_update_time']
# _refresh_statistics() should ignore the invalid statistics_data(type
# of boot_time is invalid); default data shouldn't be replaced.
arg = [('Init', 'lname', {'boot_time': 1})]
stat._refresh_statistics(arg)
self.assertEqual(
{'boot_time': self.const_default_datetime},
stat.statistics_data['Init'])
# 'last_update_time' doesn't change
self.assertEqual(
last_update_time,
stat.statistics_data['Stats']['last_update_time'])