diff --git a/ldclient/client.py b/ldclient/client.py index f0b973a2..16d91b0a 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -8,11 +8,11 @@ import traceback from ldclient.config import Config as Config -from ldclient.event_processor import NullEventProcessor from ldclient.feature_requester import FeatureRequesterImpl from ldclient.feature_store import _FeatureStoreDataSetSorter from ldclient.flag import EvaluationDetail, evaluate, error_reason from ldclient.flags_state import FeatureFlagsState +from ldclient.impl.stubs import NullEventProcessor, NullUpdateProcessor from ldclient.interfaces import FeatureStore from ldclient.polling import PollingUpdateProcessor from ldclient.streaming import StreamingUpdateProcessor @@ -94,45 +94,21 @@ def __init__(self, sdk_key=None, config=None, start_wait=5): self._store = _FeatureStoreClientWrapper(self._config.feature_store) """ :type: FeatureStore """ - if self._config.offline or not self._config.send_events: - self._event_processor = NullEventProcessor() - else: - self._event_processor = self._config.event_processor_class(self._config) - if self._config.offline: log.info("Started LaunchDarkly Client in offline mode") - return if self._config.use_ldd: log.info("Started LaunchDarkly Client in LDD mode") - return - update_processor_ready = threading.Event() - - if self._config.update_processor_class: - log.info("Using user-specified update processor: " + str(self._config.update_processor_class)) - self._update_processor = self._config.update_processor_class( - self._config, self._store, update_processor_ready) - else: - if self._config.feature_requester_class: - feature_requester = self._config.feature_requester_class(self._config) - else: - feature_requester = FeatureRequesterImpl(self._config) - """ :type: FeatureRequester """ - - if self._config.stream: - self._update_processor = StreamingUpdateProcessor( - self._config, feature_requester, self._store, update_processor_ready) - else: - log.info("Disabling streaming API") - log.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support") - self._update_processor = PollingUpdateProcessor( - self._config, feature_requester, self._store, update_processor_ready) - """ :type: UpdateProcessor """ + self._event_processor = self._make_event_processor(self._config) + update_processor_ready = threading.Event() + self._update_processor = self._make_update_processor(self._config, self._store, update_processor_ready) self._update_processor.start() - log.info("Waiting up to " + str(start_wait) + " seconds for LaunchDarkly client to initialize...") - update_processor_ready.wait(start_wait) + + if start_wait > 0 and not self._config.offline and not self._config.use_ldd: + log.info("Waiting up to " + str(start_wait) + " seconds for LaunchDarkly client to initialize...") + update_processor_ready.wait(start_wait) if self._update_processor.initialized() is True: log.info("Started LaunchDarkly Client: OK") @@ -140,6 +116,32 @@ def __init__(self, sdk_key=None, config=None, start_wait=5): log.warn("Initialization timeout exceeded for LaunchDarkly Client or an error occurred. " "Feature Flags may not yet be available.") + def _make_event_processor(self, config): + if config.offline or not config.send_events: + return NullEventProcessor() + return config.event_processor_class(config) + + def _make_update_processor(self, config, store, ready): + if config.update_processor_class: + log.info("Using user-specified update processor: " + str(config.update_processor_class)) + return config.update_processor_class(config, store, ready) + + if config.offline or config.use_ldd: + return NullUpdateProcessor(config, store, ready) + + if config.feature_requester_class: + feature_requester = config.feature_requester_class(config) + else: + feature_requester = FeatureRequesterImpl(config) + """ :type: FeatureRequester """ + + if config.stream: + return StreamingUpdateProcessor(config, feature_requester, store, ready) + + log.info("Disabling streaming API") + log.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support") + return PollingUpdateProcessor(config, feature_requester, store, ready) + def get_sdk_key(self): """Returns the configured SDK key. @@ -153,13 +155,16 @@ def close(self): Do not attempt to use the client after calling this method. """ log.info("Closing LaunchDarkly client..") - if self.is_offline(): - return - if self._event_processor: - self._event_processor.stop() - if self._update_processor and self._update_processor.is_alive(): - self._update_processor.stop() + self._event_processor.stop() + self._update_processor.stop() + # These magic methods allow a client object to be automatically cleaned up by the "with" scope operator + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + def _send_event(self, event): self._event_processor.send_event(event) diff --git a/ldclient/event_processor.py b/ldclient/event_processor.py index b5b0e370..2bd4f322 100644 --- a/ldclient/event_processor.py +++ b/ldclient/event_processor.py @@ -37,26 +37,6 @@ __USER_ATTRS_TO_STRINGIFY_FOR_EVENTS__ = [ "key", "secondary", "ip", "country", "email", "firstName", "lastName", "avatar", "name" ] -class NullEventProcessor(EventProcessor): - def __init__(self): - pass - - def start(self): - pass - - def stop(self): - pass - - def is_alive(self): - return False - - def send_event(self, event): - pass - - def flush(self): - pass - - EventProcessorMessage = namedtuple('EventProcessorMessage', ['type', 'param']) diff --git a/ldclient/impl/stubs.py b/ldclient/impl/stubs.py new file mode 100644 index 00000000..30d0eea8 --- /dev/null +++ b/ldclient/impl/stubs.py @@ -0,0 +1,39 @@ + +from ldclient.interfaces import EventProcessor, UpdateProcessor + + +class NullEventProcessor(EventProcessor): + def __init__(self): + pass + + def start(self): + pass + + def stop(self): + pass + + def is_alive(self): + return False + + def send_event(self, event): + pass + + def flush(self): + pass + + +class NullUpdateProcessor(UpdateProcessor): + def __init__(self, config, store, ready): + self._ready = ready + + def start(self): + self._ready.set() + + def stop(self): + pass + + def is_alive(self): + return False + + def initialized(self): + return True diff --git a/testing/test_ldclient.py b/testing/test_ldclient.py index 12746857..e1ee3910 100644 --- a/testing/test_ldclient.py +++ b/testing/test_ldclient.py @@ -1,8 +1,13 @@ from ldclient.client import LDClient, Config -from ldclient.event_processor import NullEventProcessor +from ldclient.event_processor import DefaultEventProcessor from ldclient.feature_store import InMemoryFeatureStore +from ldclient.impl.stubs import NullEventProcessor, NullUpdateProcessor from ldclient.interfaces import UpdateProcessor +from ldclient.polling import PollingUpdateProcessor +from ldclient.streaming import StreamingUpdateProcessor from ldclient.versioned_data_kind import FEATURES, SEGMENTS + +import logging import pytest from testing.stub_util import CapturingFeatureStore, MockEventProcessor, MockUpdateProcessor from testing.sync_util import wait_until @@ -13,14 +18,8 @@ import Queue as queue -client = LDClient(config=Config(base_uri="http://localhost:3000", - event_processor_class = MockEventProcessor, update_processor_class = MockUpdateProcessor)) -offline_client = LDClient(config= - Config(sdk_key="secret", base_uri="http://localhost:3000", - offline=True)) -no_send_events_client = LDClient(config= - Config(sdk_key="secret", base_uri="http://localhost:3000", - update_processor_class = MockUpdateProcessor, send_events=False)) +unreachable_uri="http://fake" + user = { u'key': u'xyz', @@ -30,14 +29,32 @@ } -def make_client(store): +def make_client(store = InMemoryFeatureStore()): return LDClient(config=Config(sdk_key = 'SDK_KEY', - base_uri="http://localhost:3000", + base_uri=unreachable_uri, + events_uri=unreachable_uri, + stream_uri=unreachable_uri, event_processor_class=MockEventProcessor, update_processor_class=MockUpdateProcessor, feature_store=store)) +def make_offline_client(): + return LDClient(config=Config(sdk_key="secret", + offline=True, + base_uri=unreachable_uri, + events_uri=unreachable_uri, + stream_uri=unreachable_uri)) + + +def make_ldd_client(): + return LDClient(config=Config(sdk_key="secret", + use_ldd=True, + base_uri=unreachable_uri, + events_uri=unreachable_uri, + stream_uri=unreachable_uri)) + + def make_off_flag_with_value(key, value): return { u'key': key, @@ -68,56 +85,93 @@ def test_ctor_both_sdk_keys_set(): def test_client_has_null_event_processor_if_offline(): - assert isinstance(offline_client._event_processor, NullEventProcessor) + with make_offline_client() as client: + assert isinstance(client._event_processor, NullEventProcessor) def test_client_has_null_event_processor_if_send_events_off(): - assert isinstance(no_send_events_client._event_processor, NullEventProcessor) + config = Config(sdk_key="secret", base_uri=unreachable_uri, + update_processor_class = MockUpdateProcessor, send_events=False) + with LDClient(config=config) as client: + assert isinstance(client._event_processor, NullEventProcessor) + + +def test_client_has_normal_event_processor_in_ldd_mode(): + with make_ldd_client() as client: + assert isinstance(client._event_processor, DefaultEventProcessor) + + +def test_client_has_null_update_processor_in_offline_mode(): + with make_offline_client() as client: + assert isinstance(client._update_processor, NullUpdateProcessor) + + +def test_client_has_null_update_processor_in_ldd_mode(): + with make_ldd_client() as client: + assert isinstance(client._update_processor, NullUpdateProcessor) + + +@pytest.mark.skip("Can't currently use a live stream processor in tests because its error logging will disrupt other tests.") +def test_client_has_streaming_processor_by_default(): + config = Config(sdk_key="secret", base_uri=unreachable_uri, stream_uri=unreachable_uri, send_events=False) + with LDClient(config=config, start_wait=0) as client: + assert isinstance(client._update_processor, StreamingUpdateProcessor) + + +@pytest.mark.skip("Can't currently use a live polling processor in tests because its error logging will disrupt other tests.") +def test_client_has_polling_processor_if_streaming_is_disabled(): + config = Config(sdk_key="secret", stream=False, base_uri=unreachable_uri, stream_uri=unreachable_uri, send_events=False) + with LDClient(config=config, start_wait=0) as client: + assert isinstance(client._update_processor, PollingUpdateProcessor) def test_toggle_offline(): - assert offline_client.variation('feature.key', user, default=None) is None + with make_offline_client() as client: + assert client.variation('feature.key', user, default=None) is None def test_identify(): - client.identify(user) - - e = get_first_event(client) - assert e['kind'] == 'identify' and e['key'] == u'xyz' and e['user'] == user + with make_client() as client: + client.identify(user) + e = get_first_event(client) + assert e['kind'] == 'identify' and e['key'] == u'xyz' and e['user'] == user def test_identify_no_user(): - client.identify(None) - assert count_events(client) == 0 + with make_client() as client: + client.identify(None) + assert count_events(client) == 0 def test_identify_no_user_key(): - client.identify({ 'name': 'nokey' }) - assert count_events(client) == 0 + with make_client() as client: + client.identify({ 'name': 'nokey' }) + assert count_events(client) == 0 def test_track(): - client.track('my_event', user, 42) - - e = get_first_event(client) - assert e['kind'] == 'custom' and e['key'] == 'my_event' and e['user'] == user and e['data'] == 42 + with make_client() as client: + client.track('my_event', user, 42) + e = get_first_event(client) + assert e['kind'] == 'custom' and e['key'] == 'my_event' and e['user'] == user and e['data'] == 42 def test_track_no_user(): - client.track('my_event', None) - assert count_events(client) == 0 + with make_client() as client: + client.track('my_event', None) + assert count_events(client) == 0 def test_track_no_user_key(): - client.track('my_event', { 'name': 'nokey' }) - assert count_events(client) == 0 + with make_client() as client: + client.track('my_event', { 'name': 'nokey' }) + assert count_events(client) == 0 def test_defaults(): - my_client = LDClient(config=Config(base_uri="http://localhost:3000", - defaults={"foo": "bar"}, - offline=True)) - assert "bar" == my_client.variation('foo', user, default=None) + config=Config(base_uri="http://localhost:3000", defaults={"foo": "bar"}, offline=True) + with LDClient(config=config) as client: + assert "bar" == client.variation('foo', user, default=None) def test_defaults_and_online(): @@ -144,7 +198,8 @@ def test_defaults_and_online_no_default(): def test_no_defaults(): - assert "bar" == offline_client.variation('foo', user, default="bar") + with make_offline_client() as client: + assert "bar" == client.variation('foo', user, default="bar") def test_event_for_existing_feature(): @@ -153,19 +208,19 @@ def test_event_for_existing_feature(): feature['debugEventsUntilDate'] = 1000 store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}}) - client = make_client(store) - assert 'value' == client.variation('feature.key', user, default='default') - e = get_first_event(client) - assert (e['kind'] == 'feature' and - e['key'] == 'feature.key' and - e['user'] == user and - e['version'] == feature['version'] and - e['value'] == 'value' and - e['variation'] == 0 and - e.get('reason') is None and - e['default'] == 'default' and - e['trackEvents'] == True and - e['debugEventsUntilDate'] == 1000) + with make_client(store) as client: + assert 'value' == client.variation('feature.key', user, default='default') + e = get_first_event(client) + assert (e['kind'] == 'feature' and + e['key'] == 'feature.key' and + e['user'] == user and + e['version'] == feature['version'] and + e['value'] == 'value' and + e['variation'] == 0 and + e.get('reason') is None and + e['default'] == 'default' and + e['trackEvents'] == True and + e['debugEventsUntilDate'] == 1000) def test_event_for_existing_feature_with_reason(): @@ -174,33 +229,33 @@ def test_event_for_existing_feature_with_reason(): feature['debugEventsUntilDate'] = 1000 store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}}) - client = make_client(store) - assert 'value' == client.variation_detail('feature.key', user, default='default').value - e = get_first_event(client) - assert (e['kind'] == 'feature' and - e['key'] == 'feature.key' and - e['user'] == user and - e['version'] == feature['version'] and - e['value'] == 'value' and - e['variation'] == 0 and - e['reason'] == {'kind': 'OFF'} and - e['default'] == 'default' and - e['trackEvents'] == True and - e['debugEventsUntilDate'] == 1000) + with make_client(store) as client: + assert 'value' == client.variation_detail('feature.key', user, default='default').value + e = get_first_event(client) + assert (e['kind'] == 'feature' and + e['key'] == 'feature.key' and + e['user'] == user and + e['version'] == feature['version'] and + e['value'] == 'value' and + e['variation'] == 0 and + e['reason'] == {'kind': 'OFF'} and + e['default'] == 'default' and + e['trackEvents'] == True and + e['debugEventsUntilDate'] == 1000) def test_event_for_unknown_feature(): store = InMemoryFeatureStore() store.init({FEATURES: {}}) - client = make_client(store) - assert 'default' == client.variation('feature.key', user, default='default') - e = get_first_event(client) - assert (e['kind'] == 'feature' and - e['key'] == 'feature.key' and - e['user'] == user and - e['value'] == 'default' and - e['variation'] == None and - e['default'] == 'default') + with make_client(store) as client: + assert 'default' == client.variation('feature.key', user, default='default') + e = get_first_event(client) + assert (e['kind'] == 'feature' and + e['key'] == 'feature.key' and + e['user'] == user and + e['value'] == 'default' and + e['variation'] == None and + e['default'] == 'default') def test_event_for_existing_feature_with_no_user(): @@ -209,18 +264,18 @@ def test_event_for_existing_feature_with_no_user(): feature['debugEventsUntilDate'] = 1000 store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}}) - client = make_client(store) - assert 'default' == client.variation('feature.key', None, default='default') - e = get_first_event(client) - assert (e['kind'] == 'feature' and - e['key'] == 'feature.key' and - e['user'] == None and - e['version'] == feature['version'] and - e['value'] == 'default' and - e['variation'] == None and - e['default'] == 'default' and - e['trackEvents'] == True and - e['debugEventsUntilDate'] == 1000) + with make_client(store) as client: + assert 'default' == client.variation('feature.key', None, default='default') + e = get_first_event(client) + assert (e['kind'] == 'feature' and + e['key'] == 'feature.key' and + e['user'] == None and + e['version'] == feature['version'] and + e['value'] == 'default' and + e['variation'] == None and + e['default'] == 'default' and + e['trackEvents'] == True and + e['debugEventsUntilDate'] == 1000) def test_event_for_existing_feature_with_no_user_key(): @@ -229,24 +284,25 @@ def test_event_for_existing_feature_with_no_user_key(): feature['debugEventsUntilDate'] = 1000 store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}}) - client = make_client(store) - bad_user = { u'name': u'Bob' } - assert 'default' == client.variation('feature.key', bad_user, default='default') - e = get_first_event(client) - assert (e['kind'] == 'feature' and - e['key'] == 'feature.key' and - e['user'] == bad_user and - e['version'] == feature['version'] and - e['value'] == 'default' and - e['variation'] == None and - e['default'] == 'default' and - e['trackEvents'] == True and - e['debugEventsUntilDate'] == 1000) + with make_client(store) as client: + bad_user = { u'name': u'Bob' } + assert 'default' == client.variation('feature.key', bad_user, default='default') + e = get_first_event(client) + assert (e['kind'] == 'feature' and + e['key'] == 'feature.key' and + e['user'] == bad_user and + e['version'] == feature['version'] and + e['value'] == 'default' and + e['variation'] == None and + e['default'] == 'default' and + e['trackEvents'] == True and + e['debugEventsUntilDate'] == 1000) def test_secure_mode_hash(): user = {'key': 'Message'} - assert offline_client.secure_mode_hash(user) == "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597" + with make_offline_client() as client: + assert client.secure_mode_hash(user) == "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597" dependency_ordering_test_data = { diff --git a/testing/test_ldclient_evaluation.py b/testing/test_ldclient_evaluation.py index be925a5c..f716c5de 100644 --- a/testing/test_ldclient_evaluation.py +++ b/testing/test_ldclient_evaluation.py @@ -123,7 +123,7 @@ def test_variation_detail_when_user_is_none(): expected = EvaluationDetail('default', None, {'kind': 'ERROR', 'errorKind': 'USER_NOT_SPECIFIED'}) assert expected == client.variation_detail('feature.key', None, default='default') -def test_variation_when_user_has_no_key(): +def test_variation_detail_when_user_has_no_key(): feature = make_off_flag_with_value('feature.key', 'value') store = InMemoryFeatureStore() store.init({FEATURES: {'feature.key': feature}})