Refactor and test main entry point.

plugin.py is really hard to test because good part of
its logic happend during code importation.
It's test depends on the base TestCase that mocks too
much thinks in a way that makes hard to spot real problems.
The test also fails to test re-authenthication bug because
of bad mocking.

This code should fix above problems. It also remove some
logic in the hooks: exceptions are already captured and
logged by collectd and there is no need doing it inside of
hooks. Init hook is also pointless from the perspective
of this plugin initialization and it was removed.

Co-authored-by: Emma Foley <emma.l.foley@intel.com>
Related-Bug: #1615349
Change-Id: I4db8a94243ecbe98cd6bc13e4b66293172346dcd
This commit is contained in:
Federico Ressi
2016-08-23 22:54:44 +01:00
committed by Emma Foley
parent 0d03fa8139
commit 9cf7d56f5a
4 changed files with 448 additions and 267 deletions

View File

@@ -13,81 +13,72 @@
# under the License. # under the License.
"""Ceilometer collectd plugin""" """Ceilometer collectd plugin"""
from __future__ import unicode_literals import logging
# pylint: disable=import-error try:
import collectd # pylint: disable=import-error
# pylint: enable=import-error import collectd
# pylint: enable=import-error
except ImportError:
collectd = None # when running unit tests collectd is not avaliable
import collectd_ceilometer
from collectd_ceilometer.logger import CollectdLogHandler from collectd_ceilometer.logger import CollectdLogHandler
from collectd_ceilometer.meters import MeterStorage from collectd_ceilometer.meters import MeterStorage
from collectd_ceilometer.settings import Config from collectd_ceilometer.settings import Config
from collectd_ceilometer.writer import Writer from collectd_ceilometer.writer import Writer
import logging
log_handler = CollectdLogHandler(collectd=collectd)
logging.getLogger().addHandler(log_handler)
logging.getLogger().setLevel(logging.NOTSET)
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
ROOT_LOGGER = logging.getLogger(collectd_ceilometer.__name__)
def register_plugin(collectd):
"Bind plugin hooks to collectd and viceversa"
config = Config.instance()
# Setup loggging
log_handler = CollectdLogHandler(collectd=collectd)
log_handler.cfg = config
ROOT_LOGGER.addHandler(log_handler)
ROOT_LOGGER.setLevel(logging.NOTSET)
# Creates collectd plugin instance
instance = Plugin(collectd=collectd, config=config)
# Register plugin callbacks
collectd.register_config(instance.config)
collectd.register_write(instance.write)
collectd.register_shutdown(instance.shutdown)
class Plugin(object): class Plugin(object):
"""Ceilometer plugin with collectd callbacks""" """Ceilometer plugin with collectd callbacks"""
# NOTE: this is multithreaded class # NOTE: this is multithreaded class
def __init__(self): def __init__(self, collectd, config):
self._meters = None self._config = config
self._writer = None self._meters = MeterStorage(collectd=collectd)
logging.getLogger("requests").setLevel(logging.WARNING) self._writer = Writer(self._meters, config=config)
def config(self, cfg): def config(self, cfg):
"""Configuration callback """Configuration callback
@param cfg configuration node provided by collectd @param cfg configuration node provided by collectd
""" """
# pylint: disable=no-self-use
config = Config.instance()
config.read(cfg)
# apply configuration self._config.read(cfg)
log_handler.verbose = config.VERBOSE
def init(self):
"""Initialization callback"""
collectd.info('Initializing the collectd OpenStack python plugin')
self._meters = MeterStorage(collectd=collectd)
self._writer = Writer(self._meters)
def write(self, vl, data=None): def write(self, vl, data=None):
"""Collectd write callback""" """Collectd write callback"""
# pylint: disable=broad-except self._writer.write(vl, data)
# pass arguments to the writer
try:
self._writer.write(vl, data)
except Exception as exc:
if collectd is not None:
collectd.error('Exception during write: %s' % exc)
def shutdown(self): def shutdown(self):
"""Shutdown callback""" """Shutdown callback"""
# pylint: disable=broad-except LOGGER.info("SHUTDOWN")
collectd.info("SHUTDOWN") self._writer.flush()
try:
self._writer.flush()
except Exception as exc:
if collectd is not None:
collectd.error('Exception during shutdown: %s' % exc)
# The collectd plugin instance if collectd:
# pylint: disable=invalid-name register_plugin(collectd=collectd)
instance = Plugin()
# pylint: enable=invalid-name
# Register plugin callbacks
collectd.register_init(instance.init)
collectd.register_config(instance.config)
collectd.register_write(instance.write)
collectd.register_shutdown(instance.shutdown)

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 Intel Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import fnmatch
from json import loads
import re
def json(obj):
return MatchJson(obj)
class MatchJson(object):
def __init__(self, obj):
self._obj = obj
def __eq__(self, json_text):
return self._obj == loads(json_text)
def __repr__(self):
return "MatchJson({})".format(repr(self._obj))
def wildcard(text):
return MatchWildcard(text)
class MatchWildcard(object):
def __init__(self, obj):
self._text = text = str(obj)
self._reg = re.compile(fnmatch.translate(text))
def __eq__(self, obj):
return self._reg.match(str(obj))
def __repr__(self):
return "MatchWildcard({})".format(self._text)

View File

@@ -14,233 +14,309 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""Plugin tests
"""Plugin tests""" """
from __future__ import unicode_literals
from collectd_ceilometer.tests.base import TestCase
from collectd_ceilometer.tests.base import Value
from collections import namedtuple from collections import namedtuple
import json import logging
import mock
import requests import requests
import unittest
import mock
from collectd_ceilometer import keystone_light
from collectd_ceilometer import plugin
from collectd_ceilometer import sender
from collectd_ceilometer.tests import match
class PluginTest(TestCase): Logger = logging.getLoggerClass()
def mock_collectd(**kwargs):
"Returns collecd module with collecd logging hooks."
return mock.patch(
__name__ + '.' + MockedCollectd.__name__, specs=True,
get_dataset=mock.MagicMock(side_effect=Exception), **kwargs)
class MockedCollectd(object):
"Mocked collectd module specifications."
def debug(self, record):
"Hook for debug messages"
def info(self, record):
"Hook for info messages"
def warning(self, record):
"Hook for warning messages"
def error(self, record):
"Hook for error messages"
def register_init(self, hook):
"Register an hook for init."
def register_config(self, hook):
"Register an hook for config."
def register_write(self, hook):
"Register an hook for write."
def register_shutdown(self, hook):
"Register an hook for shutdown."
def get_dataset(self, s):
"Gets a dataset."
def mock_config(BATCH_SIZE=1, **kwargs):
"Returns collecd module with collecd logging hooks."
return mock.patch(
__name__ + '.' + MockedConfig.__name__, specs=True,
BATCH_SIZE=BATCH_SIZE, **kwargs)
class MockedConfig(object):
"Mocked config class."
BATCH_SIZE = 1
OS_AUTH_URL = "http://test-url"
def mock_value(
host='localhost', plugin='cpu', plugin_instance='0',
_type='freq', type_instance=None, time=123456789, values=(1234,),
**kwargs):
"""Create a mock value"""
return mock.patch(
__name__ + '.' + MockedValue.__name__, specs=True,
host=host, plugin=plugin, plugin_instance=plugin_instance, type=_type,
type_instance=type_instance, time=time, values=list(values), meta=None,
**kwargs)
class MockedValue(object):
"""Value used for testing"""
host = 'localhost'
plugin = None
plugin_instance = None
type = None
type_instance = None
time = 123456789
values = []
meta = None
class TestPlugin(unittest.TestCase):
"""Test the collectd plugin""" """Test the collectd plugin"""
def setUp(self): @mock.patch.object(plugin, 'Plugin', autospec=True)
super(PluginTest, self).setUp() @mock.patch.object(plugin, 'Config', autospec=True)
client_class \ @mock.patch.object(plugin, 'CollectdLogHandler', autospec=True)
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2 @mock.patch.object(plugin, 'ROOT_LOGGER', autospec=True)
client_class.return_value\ @mock_collectd()
.get_service_endpoint.return_value = "https://test-ceilometer.tld" def test_callbacks(
self, collectd, ROOT_LOGGER, CollectdLogHandler, Config, Plugin):
# TODO(emma-l-foley): Import at top and mock here
from collectd_ceilometer.plugin import instance
from collectd_ceilometer.plugin import Plugin
self.default_instance = instance
self.plugin_instance = Plugin()
self.maxDiff = None
def test_callbacks(self):
"""Verify that the callbacks are registered properly""" """Verify that the callbacks are registered properly"""
collectd = self.get_mock('collectd') # When plugin function is called
plugin.register_plugin(collectd=collectd)
self.assertTrue(collectd.register_init.called) # Logger handler is set up
self.assertTrue(collectd.register_config.called) ROOT_LOGGER.addHandler.assert_called_once_with(
self.assertTrue(collectd.register_write.called) CollectdLogHandler.return_value)
self.assertTrue(collectd.register_shutdown.called) ROOT_LOGGER.setLevel.assert_called_once_with(logging.NOTSET)
# It create a plugin
Plugin.assert_called_once_with(
collectd=collectd, config=Config.instance.return_value)
# callbacks are registered to collectd
instance = Plugin.return_value
collectd.register_config.assert_called_once_with(instance.config)
collectd.register_write.assert_called_once_with(instance.write)
collectd.register_shutdown.assert_called_once_with(instance.shutdown)
@mock.patch.object(requests, 'post', spec=callable) @mock.patch.object(requests, 'post', spec=callable)
def test_write(self, post): @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock_collectd()
@mock_config(BATCH_SIZE=2)
@mock_value()
def test_write(self, data, config, collectd, ClientV3, post):
"""Test collectd data writing""" """Test collectd data writing"""
from collectd_ceilometer.sender import HTTP_CREATED
post.return_value = response = requests.Response() auth_client = ClientV3.return_value
response.status_code = HTTP_CREATED auth_client.get_service_endpoint.return_value =\
'https://test-ceilometer.tld'
client_class \ post.return_value.status_code = sender.HTTP_CREATED
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2 post.return_value.text = 'Created'
auth_token = client_class.return_value.auth_token
# create a value # init instance
data = self._create_value() instance = plugin.Plugin(collectd=collectd, config=config)
# set batch size to 2 and init instance
self.config.update_value('BATCH_SIZE', 2)
self._init_instance()
# no authentication has been performed so far # no authentication has been performed so far
self.assertFalse(client_class.called) ClientV3.assert_not_called()
# write first value # write first value
self._write_value(data) instance.write(data)
collectd.error.assert_not_called()
# no value has been sent to ceilometer # no value has been sent to ceilometer
post.assert_not_called() post.assert_not_called()
# send the second value # send the second value
self._write_value(data) instance.write(data)
collectd.error.assert_not_called()
# authentication client has been created # authentication client has been created
self.assertTrue(client_class.called) ClientV3.assert_called_once()
self.assertEqual(client_class.call_count, 1)
# and values has been sent # and values has been sent
post.assert_called_once() post.assert_called_once_with(
'https://test-ceilometer.tld/v2/meters/cpu.freq',
expected_args = ('https://test-ceilometer.tld/v2/meters/cpu.freq',) data=match.json([
expected_kwargs = { {"source": "collectd",
'data': [{ "counter_name": "cpu.freq",
"source": "collectd", "counter_unit": "jiffies",
"counter_name": "cpu.freq", "counter_volume": 1234,
"counter_unit": "jiffies", "timestamp": "Thu Nov 29 21:33:09 1973",
"counter_volume": 1234, "resource_id": "localhost-0",
"timestamp": "Thu Nov 29 21:33:09 1973", "resource_metadata": None,
"resource_id": "localhost-0", "counter_type": "gauge"},
"resource_metadata": None, {"source": "collectd",
"counter_type": "gauge" "counter_name": "cpu.freq",
}, { "counter_unit": "jiffies",
"source": "collectd", "counter_volume": 1234,
"counter_name": "cpu.freq", "timestamp": "Thu Nov 29 21:33:09 1973",
"counter_unit": "jiffies", "resource_id": "localhost-0",
"counter_volume": 1234, "resource_metadata": None,
"timestamp": "Thu Nov 29 21:33:09 1973", "counter_type": "gauge"}]),
"resource_id": "localhost-0", headers={'Content-type': 'application/json',
"resource_metadata": None, 'X-Auth-Token': auth_client.auth_token},
"counter_type": "gauge"}], timeout=1.0)
'headers': {
'Content-type': u'application/json',
'X-Auth-Token': auth_token},
'timeout': 1.0}
# we cannot compare JSON directly because the original data
# dictionary is unordered
called_kwargs = post.call_args[1]
called_kwargs['data'] = json.loads(called_kwargs['data'])
# verify data sent to ceilometer
self.assertEqual(post.call_args[0], expected_args)
self.assertEqual(called_kwargs, expected_kwargs)
# reset post method # reset post method
post.reset_mock() post.reset_mock()
# write another values # write another values
self._write_value(data) instance.write(data)
collectd.error.assert_not_called()
# nothing has been sent # nothing has been sent
post.assert_not_called() post.assert_not_called()
# call shutdown # call shutdown
self.plugin_instance.shutdown() instance.shutdown()
self.assertNoError()
# no errors
collectd.error.assert_not_called()
# previously written value has been sent # previously written value has been sent
post.assert_called_once() post.assert_called_once_with(
# no more authentication required 'https://test-ceilometer.tld/v2/meters/cpu.freq',
self.assertEqual(client_class.call_count, 1) data=match.json([
{"source": "collectd",
expected_kwargs = { "counter_name": "cpu.freq",
'data': [{ "counter_unit": "jiffies",
"source": "collectd", "counter_volume": 1234,
"counter_name": "cpu.freq", "timestamp": "Thu Nov 29 21:33:09 1973",
"counter_unit": "jiffies", "resource_id": "localhost-0",
"counter_volume": 1234, "resource_metadata": None,
"timestamp": "Thu Nov 29 21:33:09 1973", "counter_type": "gauge"}]),
"resource_id": "localhost-0", headers={
"resource_metadata": None, 'Content-type': 'application/json',
"counter_type": "gauge"}], 'X-Auth-Token': auth_client.auth_token},
'headers': { timeout=1.0)
'Content-type': u'application/json',
'X-Auth-Token': auth_token},
'timeout': 1.0}
# we cannot compare JSON directly because the original data
# dictionary is unordered
called_kwargs = post.call_args[1]
called_kwargs['data'] = json.loads(called_kwargs['data'])
# verify data sent to ceilometer
self.assertEqual(post.call_args[0], expected_args)
self.assertEqual(called_kwargs, expected_kwargs)
@mock.patch.object(requests, 'post', spec=callable) @mock.patch.object(requests, 'post', spec=callable)
def test_write_auth_failed(self, post): @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock.patch.object(plugin, 'LOGGER', autospec=True)
@mock_collectd()
@mock_config()
@mock_value()
def test_write_auth_failed(
self, data, config, collectd, LOGGER, ClientV3, post):
"""Test authentication failure""" """Test authentication failure"""
ClientV3.auth_url = "http://tst-url"
# tell the auth client to rise an exception # tell the auth client to rise an exception
client_class \ ClientV3.side_effect = RuntimeError('Test Client() exception')
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
client_class.side_effect = Exception('Test Client() exception')
# init instance # init instance
self._init_instance() instance = plugin.Plugin(collectd=collectd, config=config)
# write the value # write the value
errors = [ self.assertRaises(RuntimeError, instance.write, data)
'Exception during write: Test Client() exception']
self._write_value(self._create_value(), errors)
# no requests method has been called # no requests method has been called
post.assert_not_called() post.assert_not_called()
@mock.patch.object(requests, 'post', spec=callable) @mock.patch.object(requests, 'post', spec=callable)
def test_write_auth_failed2(self, post): @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock.patch.object(sender, 'LOGGER', autospec=True)
@mock_collectd()
@mock_config()
@mock_value()
def test_write_auth_failed2(
self, data, config, collectd, LOGGER, ClientV3, post):
"""Test authentication failure2""" """Test authentication failure2"""
# tell the auth client to rise an exception ClientV3.side_effect = keystone_light.KeystoneException(
keystone \
= self.get_mock('collectd_ceilometer.keystone_light')
client_class = keystone.ClientV2
client_class.side_effect = keystone.KeystoneException(
"Missing name 'xxx' in received services", "Missing name 'xxx' in received services",
"exception", "exception",
"services list") "services list")
# init instance # init instance
self._init_instance() instance = plugin.Plugin(collectd=collectd, config=config)
# write the value # write the value
errors = [ instance.write(data)
"Suspending error logs until successful auth",
"Authentication error: Missing name 'xxx' in received services" LOGGER.error.assert_called_once_with(
"\nReason: exception"] "Suspending error logs until successful auth")
self._write_value(self._create_value(), errors) LOGGER.log.assert_called_once_with(
logging.ERROR, "Authentication error: %s",
"Missing name 'xxx' in received services\nReason: exception",
exc_info=0)
# no requests method has been called # no requests method has been called
post.assert_not_called() post.assert_not_called()
@mock.patch.object(requests, 'post', spec=callable) @mock.patch.object(requests, 'post', spec=callable)
def test_request_error(self, post): @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock_collectd()
@mock_config()
@mock_value()
def test_request_error(
self, data, config, collectd, ClientV3, post):
"""Test error raised by underlying requests module""" """Test error raised by underlying requests module"""
# we have to import the RequestException here as it has been mocked
from requests.exceptions import RequestException
# tell POST request to raise an exception # tell POST request to raise an exception
post.side_effect = RequestException('Test POST exception') post.side_effect = requests.RequestException('Test POST exception')
# init instance # init instance
self._init_instance() instance = plugin.Plugin(collectd=collectd, config=config)
# write the value # write the value
self._write_value( self.assertRaises(requests.RequestException, instance.write, data)
self._create_value(),
['Exception during write: Test POST exception'])
@mock.patch.object(sender.Sender, '_perform_request', spec=callable)
@mock.patch.object(requests, 'post', spec=callable) @mock.patch.object(requests, 'post', spec=callable)
def test_reauthentication(self, post): @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock_collectd()
@mock_config()
@mock_value()
def test_reauthentication(self, data, config, collectd,
ClientV3, post, perf_req):
"""Test re-authentication""" """Test re-authentication"""
client_class \
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
client_class.return_value.auth_token = 'Test auth token'
# init instance
self._init_instance()
# response returned on success # response returned on success
response_ok = requests.Response() response_ok = requests.Response()
response_ok.status_code = requests.codes["OK"] response_ok.status_code = requests.codes["OK"]
@@ -251,35 +327,48 @@ class PluginTest(TestCase):
# write the first value with success # write the first value with success
# subsequent call of POST method will fail due to the authentication # subsequent call of POST method will fail due to the authentication
post.side_effect = [response_ok, response_unauthorized, response_ok] perf_req.return_value = response_ok
self._write_value(self._create_value())
client = ClientV3.return_value
client.auth_url = "http://tst-url"
client.auth_token = 'Test auth token'
# init instance
instance = plugin.Plugin(collectd=collectd, config=config)
# write the value
instance.write(data)
# verify the auth token # verify the auth token
call_list = post.call_args_list perf_req.assert_called_once_with(
self.assertEqual(len(call_list), 1) mock.ANY, mock.ANY,
# 0 = first call > 1 = call kwargs > headers argument > auth token 'Test auth token')
token = call_list[0][1]['headers']['X-Auth-Token']
self.assertEqual(token, 'Test auth token')
# set a new auth token # set a new auth token
client_class.return_value.auth_token = 'New test auth token' client.auth_token = 'New test auth token'
perf_req.side_effect = \
[requests.exceptions.HTTPError(response=response_unauthorized),
response_ok]
self._write_value(self._create_value()) # write the value
instance.write(data)
# verify the auth token # verify the auth token
call_list = post.call_args_list perf_req.assert_has_calls([
mock.call(mock.ANY, mock.ANY,
# POST called three times 'Test auth token'),
self.assertEqual(len(call_list), 3) mock.call(mock.ANY, mock.ANY,
# the second call contains the old token 'New test auth token')
token = call_list[1][1]['headers']['X-Auth-Token'] ])
self.assertEqual(token, 'Test auth token')
# the third call contains the new token
token = call_list[2][1]['headers']['X-Auth-Token']
self.assertEqual(token, 'New test auth token')
@mock.patch.object(requests, 'post', spec=callable) @mock.patch.object(requests, 'post', spec=callable)
def test_authentication_in_multiple_threads(self, post): @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock.patch.object(plugin, 'LOGGER', autospec=True)
@mock_collectd()
@mock_config()
@mock_value()
def test_authentication_in_multiple_threads(
self, data, config, collectd, LOGGER, ClientV3, post):
"""Test authentication in muliple threads """Test authentication in muliple threads
This test simulates the authentication performed from different thread This test simulates the authentication performed from different thread
@@ -289,17 +378,18 @@ class PluginTest(TestCase):
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
# init plugin instance # init instance
self._init_instance() instance = plugin.Plugin(collectd=collectd, config=config)
# the sender used by the instance # the sender used by the instance
sender = self.plugin_instance._writer._sender sender = instance._writer._sender
# create a dummy lock # create a dummy lock
class DummyLock(namedtuple('LockBase', ['sender', 'token', 'urlbase'])): class DummyLock(namedtuple('LockBase', ['sender', 'token', 'urlbase'])):
"""Lock simulation, which sets the auth token when locked""" """Lock simulation, which sets the auth token when locked"""
def __enter__(self, *args, **kwargs): def __enter__(self, *args, **kwargs):
# pylint: disable=protected-access
self.sender._auth_token = self.token self.sender._auth_token = self.token
self.sender._url_base = self.urlbase self.sender._url_base = self.urlbase
@@ -310,61 +400,110 @@ class PluginTest(TestCase):
sender._auth_lock = DummyLock(sender, 'TOKEN', 'URLBASE/%s') sender._auth_lock = DummyLock(sender, 'TOKEN', 'URLBASE/%s')
# write the value # write the value
self._write_value(self._create_value()) instance.write(data)
# verify the results # No errors has been registered
client_class \ LOGGER.exception.assert_not_called()
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
# client has not been called at all # client has not been called at all
self.assertFalse(client_class.called) ClientV3.assert_not_called()
# verify the auth token # verify the auth token
call_list = post.call_args_list post.assert_called_once_with(
self.assertEqual(len(call_list), 1) 'URLBASE/cpu.freq', data=mock.ANY,
# 0 = first call > 1 = call kwargs > headers argument > auth token headers={
token = call_list[0][1]['headers']['X-Auth-Token'] 'Content-type': 'application/json', 'X-Auth-Token': 'TOKEN'},
self.assertEqual(token, 'TOKEN') timeout=1.0)
def test_exceptions(self): @mock.patch.object(requests, 'post', spec=callable)
@mock.patch.object(sender, 'ClientV3', autospec=True)
@mock.patch.object(plugin, 'Writer', autospec=True)
@mock.patch.object(plugin, 'LOGGER', autospec=True)
@mock_collectd()
@mock_config()
@mock_value()
def test_exceptions(
self, data, config, collectd, LOGGER, Writer, ClientV3, post):
"""Test exception raised during write and shutdown""" """Test exception raised during write and shutdown"""
self._init_instance() writer = Writer.return_value
writer.write.side_effect = ValueError('Test write error')
writer.flush.side_effect = RuntimeError('Test shutdown error')
writer = mock.Mock() # init instance
writer.flush.side_effect = Exception('Test shutdown error') instance = plugin.Plugin(collectd=collectd, config=config)
writer.write.side_effect = Exception('Test write error')
# pylint: disable=protected-access self.assertRaises(ValueError, instance.write, data)
self.plugin_instance._writer = writer self.assertRaises(RuntimeError, instance.shutdown)
# pylint: enable=protected-access
self.plugin_instance.write(self._create_value()) @mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
self.plugin_instance.shutdown() @mock_collectd()
def test_log_debug_to_collectd(self, collectd, ROOT_LOGGER):
"""Verify that debug messages are sent to collectd."""
self.assertErrors([ plugin.register_plugin(collectd=collectd)
'Exception during write: Test write error',
'Exception during shutdown: Test shutdown error'])
@staticmethod # When log messages are produced
def _create_value(): ROOT_LOGGER.debug('some %s', 'noise')
"""Create a value"""
retval = Value()
retval.plugin = 'cpu'
retval.plugin_instance = '0'
retval.type = 'freq'
retval.add_value(1234)
return retval
def _init_instance(self): # When plugin function is called
"""Init current plugin instance""" collectd.debug.assert_called_once_with('some noise')
self.plugin_instance.config(self.config.node)
self.plugin_instance.init()
def _write_value(self, value, errors=None): @mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
"""Write a value and verify result""" @mock_collectd()
self.plugin_instance.write(value) def test_log_infos_to_collectd(self, collectd, ROOT_LOGGER):
if errors is None: """Verify that the callbacks are registered properly"""
self.assertNoError()
else: plugin.register_plugin(collectd=collectd)
self.assertErrors(errors)
# When log messages are produced
ROOT_LOGGER.info('%d info', 1)
# When plugin function is called
collectd.info.assert_called_once_with('1 info')
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
@mock_collectd()
def test_log_errors_to_collectd(self, collectd, ROOT_LOGGER):
"""Verify that the callbacks are registered properly"""
plugin.register_plugin(collectd=collectd)
# When log messages are produced
ROOT_LOGGER.error('some error')
# When plugin function is called
collectd.error.assert_called_once_with('some error')
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
@mock_collectd()
def test_log_fatal_to_collectd(self, collectd, ROOT_LOGGER):
"""Verify that the callbacks are registered properly"""
plugin.register_plugin(collectd=collectd)
# When log messages are produced
ROOT_LOGGER.fatal('some error')
# When plugin function is called
collectd.error.assert_called_once_with('some error')
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
@mock_collectd()
def test_log_exceptions_to_collectd(self, collectd, ROOT_LOGGER):
"""Verify that the callbacks are registered properly"""
plugin.register_plugin(collectd=collectd)
# When exception is logged
try:
raise ValueError('some error')
except ValueError:
ROOT_LOGGER.exception('got exception')
# When main function is called
collectd.error.assert_called_once_with(
match.wildcard('got exception\n'
'Traceback (most recent call last):\n'
'*'
'ValueError: some error'))

View File

@@ -16,7 +16,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collectd_ceilometer.sender import Sender from collectd_ceilometer.sender import Sender
from collectd_ceilometer.settings import Config
from collections import defaultdict from collections import defaultdict
from collections import namedtuple from collections import namedtuple
import json import json
@@ -87,10 +86,11 @@ class SampleContainer(object):
class Writer(object): class Writer(object):
"""Data collector""" """Data collector"""
def __init__(self, meters): def __init__(self, meters, config):
self._meters = meters self._meters = meters
self._samples = SampleContainer() self._samples = SampleContainer()
self._sender = Sender() self._sender = Sender()
self._config = config
def write(self, vl, data): def write(self, vl, data):
"""Collect data from collectd """Collect data from collectd
@@ -123,8 +123,7 @@ class Writer(object):
] ]
# add data to cache and get the samples to send # add data to cache and get the samples to send
to_send = self._samples.add(metername, data, to_send = self._samples.add(metername, data, self._config.BATCH_SIZE)
Config.instance().BATCH_SIZE)
if to_send: if to_send:
self._send_data(metername, to_send) self._send_data(metername, to_send)