| # Copyright 2017-present Open Networking Foundation |
| # |
| # 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. |
| |
| from __future__ import absolute_import, print_function |
| import confluent_kafka |
| import functools |
| import unittest |
| |
| from mock import patch, PropertyMock, ANY |
| |
| import os |
| import sys |
| import time |
| |
| log = None |
| |
| test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) |
| sync_lib_dir = os.path.join(test_path, "..", "xossynchronizer") |
| xos_dir = os.path.join(test_path, "..", "..", "..", "xos") |
| |
| print(os.getcwd()) |
| |
| def config_get_mock(orig, overrides, key): |
| if key in overrides: |
| return overrides[key] |
| else: |
| return orig(key) |
| |
| |
| class FakeKafkaConsumer: |
| def __init__(self, values=[]): |
| self.values = values |
| |
| def subscribe(self, topics): |
| pass |
| |
| def poll(self, timeout=1.0): |
| if self.values: |
| return FakeKafkaMessage(self.values.pop()) |
| # block forever |
| time.sleep(1000) |
| |
| |
| class FakeKafkaMessage: |
| """ Works like Message in confluent_kafka |
| https://docs.confluent.io/current/clients/confluent-kafka-python/#message |
| """ |
| |
| def __init__( |
| self, |
| timestamp=None, |
| topic="faketopic", |
| key="fakekey", |
| value="fakevalue", |
| error=False, |
| ): |
| |
| if timestamp is None: |
| self.fake_ts_type = confluent_kafka.TIMESTAMP_NOT_AVAILABLE |
| self.fake_timestamp = None |
| else: |
| self.fake_ts_type = confluent_kafka.TIMESTAMP_CREATE_TIME |
| self.fake_timestamp = timestamp |
| |
| self.fake_topic = topic |
| self.fake_key = key |
| self.fake_value = value |
| self.fake_error = error |
| |
| def error(self): |
| return self.fake_error |
| |
| def timestamp(self): |
| return (self.fake_ts_type, self.fake_timestamp) |
| |
| def topic(self): |
| return self.fake_topic |
| |
| def key(self): |
| return self.fake_key |
| |
| def value(self): |
| return self.fake_value |
| |
| |
| class TestEventEngine(unittest.TestCase): |
| @classmethod |
| def setUpClass(cls): |
| |
| global log |
| |
| config = os.path.join(test_path, "test_config.yaml") |
| from xosconfig import Config |
| |
| Config.clear() |
| Config.init(config, "synchronizer-config-schema.yaml") |
| |
| if not log: |
| from multistructlog import create_logger |
| |
| log = create_logger(Config().get("logging")) |
| |
| def setUp(self): |
| global XOSKafkaThread, Config, log |
| |
| self.sys_path_save = sys.path |
| self.cwd_save = os.getcwd() |
| |
| config = os.path.join(test_path, "test_config.yaml") |
| from xosconfig import Config |
| |
| Config.clear() |
| Config.init(config, "synchronizer-config-schema.yaml") |
| |
| from xossynchronizer.mock_modelaccessor_build import ( |
| build_mock_modelaccessor, |
| ) |
| |
| build_mock_modelaccessor(sync_lib_dir, xos_dir, services_dir=None, service_xprotos=[]) |
| |
| from xossynchronizer.modelaccessor import model_accessor |
| |
| # The test config.yaml references files in `xos-synchronizer-tests/` so make sure we're in the parent |
| # directory of the test directory. |
| os.chdir(os.path.join(test_path, "..")) |
| |
| from xossynchronizer.event_engine import XOSKafkaThread, XOSEventEngine |
| |
| self.event_steps_dir = Config.get("event_steps_dir") |
| self.event_engine = XOSEventEngine(model_accessor=model_accessor, log=log) |
| |
| def tearDown(self): |
| sys.path = self.sys_path_save |
| os.chdir(self.cwd_save) |
| |
| def test_load_event_step_modules(self): |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| self.assertEqual(len(self.event_engine.event_steps), 1) |
| |
| def test_start(self): |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| FakeKafkaConsumer, "subscribe" |
| ) as fake_subscribe, patch.object( |
| self.event_engine.event_steps[0], "process_event" |
| ) as process_event: |
| |
| create_kafka_consumer.return_value = FakeKafkaConsumer( |
| values=["sampleevent"] |
| ) |
| self.event_engine.start() |
| |
| self.assertEqual(len(self.event_engine.threads), 1) |
| |
| # Since event_engine.start() launches threads, give them a hundred milliseconds to do something... |
| time.sleep(0.1) |
| |
| # We should have subscribed to the fake consumer |
| fake_subscribe.assert_called_once() |
| |
| # The fake consumer will have returned one event |
| process_event.assert_called_once() |
| |
| def test_start_with_pattern(self): |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| FakeKafkaConsumer, "subscribe" |
| ) as fake_subscribe, patch.object( |
| self.event_engine.event_steps[0], "process_event" |
| ) as process_event, patch.object( |
| self.event_engine.event_steps[0], "pattern", new_callable=PropertyMock |
| ) as pattern, patch.object( |
| self.event_engine.event_steps[0], "topics", new_callable=PropertyMock |
| ) as topics: |
| |
| pattern.return_value = "somepattern" |
| topics.return_value = [] |
| |
| create_kafka_consumer.return_value = FakeKafkaConsumer( |
| values=["sampleevent"] |
| ) |
| self.event_engine.start() |
| |
| self.assertEqual(len(self.event_engine.threads), 1) |
| |
| # Since event_engine.start() launches threads, give them a hundred milliseconds to do something... |
| time.sleep(0.1) |
| |
| # We should have subscribed to the fake consumer |
| fake_subscribe.assert_called_with("somepattern") |
| |
| # The fake consumer will have returned one event |
| process_event.assert_called_once() |
| |
| def test_start_bad_tech(self): |
| """ Set an unknown Technology in the event_step. XOSEventEngine.start() should print an error message and |
| not create any threads. |
| """ |
| |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| log, "error" |
| ) as log_error, patch.object( |
| self.event_engine.event_steps[0], "technology" |
| ) as technology: |
| technology.return_value = "not_kafka" |
| create_kafka_consumer.return_value = FakeKafkaConsumer() |
| self.event_engine.start() |
| |
| self.assertEqual(len(self.event_engine.threads), 0) |
| |
| log_error.assert_called_with( |
| "Unknown technology. Skipping step", |
| step="TestEventStep", |
| technology=ANY, |
| ) |
| |
| def test_start_bad_no_topics(self): |
| """ Set no topics in the event_step. XOSEventEngine.start() will launch a thread, but the thread will fail |
| with an exception before calling subscribe. |
| """ |
| |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| FakeKafkaConsumer, "subscribe" |
| ) as fake_subscribe, patch.object( |
| self.event_engine.event_steps[0], "topics", new_callable=PropertyMock |
| ) as topics: |
| topics.return_value = [] |
| create_kafka_consumer.return_value = FakeKafkaConsumer() |
| self.event_engine.start() |
| |
| # the thread does get launched, but it will fail with an exception |
| self.assertEqual(len(self.event_engine.threads), 1) |
| |
| time.sleep(0.1) |
| |
| fake_subscribe.assert_not_called() |
| |
| def test_start_bad_topics_and_pattern(self): |
| """ Set no topics in the event_step. XOSEventEngine.start() will launch a thread, but the thread will fail |
| with an exception before calling subscribe. |
| """ |
| |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| FakeKafkaConsumer, "subscribe" |
| ) as fake_subscribe, patch.object( |
| self.event_engine.event_steps[0], "pattern", new_callable=PropertyMock |
| ) as pattern: |
| pattern.return_value = "foo" |
| create_kafka_consumer.return_value = FakeKafkaConsumer() |
| self.event_engine.start() |
| |
| # the thread does get launched, but it will fail with an exception |
| self.assertEqual(len(self.event_engine.threads), 1) |
| |
| time.sleep(0.1) |
| |
| fake_subscribe.assert_not_called() |
| |
| def test_start_config_no_eventbus_kind(self): |
| """ Set a blank event_bus.kind in Config. XOSEventEngine.start() should print an error message and |
| not create any threads. |
| """ |
| |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| config_get_orig = Config.get |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| log, "error" |
| ) as log_error, patch.object( |
| Config, |
| "get", |
| new=functools.partial( |
| config_get_mock, config_get_orig, {"event_bus.kind": None} |
| ), |
| ): |
| |
| create_kafka_consumer.return_value = FakeKafkaConsumer() |
| self.event_engine.start() |
| |
| self.assertEqual(len(self.event_engine.threads), 0) |
| |
| log_error.assert_called_with( |
| "Eventbus kind is not configured in synchronizer config file." |
| ) |
| |
| def test_start_config_bad_eventbus_kind(self): |
| """ Set an unknown event_bus.kind in Config. XOSEventEngine.start() should print an error message and |
| not create any threads. |
| """ |
| |
| self.event_engine.load_event_step_modules(self.event_steps_dir) |
| |
| config_get_orig = Config.get |
| with patch.object( |
| XOSKafkaThread, "create_kafka_consumer" |
| ) as create_kafka_consumer, patch.object( |
| log, "error" |
| ) as log_error, patch.object( |
| Config, |
| "get", |
| new=functools.partial( |
| config_get_mock, config_get_orig, {"event_bus.kind": "not_kafka"} |
| ), |
| ): |
| create_kafka_consumer.return_value = FakeKafkaConsumer() |
| self.event_engine.start() |
| |
| self.assertEqual(len(self.event_engine.threads), 0) |
| |
| log_error.assert_called_with( |
| "Eventbus kind is set to a technology we do not implement.", |
| eventbus_kind="not_kafka", |
| ) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |