def test_get_treatment(self, mocker): """Test get_treatment execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) telemetry_storage = mocker.Mock(spec=TelemetryStorage) def _get_storage_mock(name): return { 'splits': split_storage, 'segments': segment_storage, 'impressions': impression_storage, 'events': event_storage, 'telemetry': telemetry_storage }[name] destroyed_property = mocker.PropertyMock() destroyed_property.return_value = False factory = mocker.Mock(spec=SplitFactory) factory._get_storage.side_effect = _get_storage_mock type(factory).destroyed = destroyed_property mocker.patch('splitio.client.client.time.time', new=lambda: 1) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) client = Client(factory, True, None) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.evaluate_treatment.return_value = { 'treatment': 'on', 'configurations': None, 'impression': { 'label': 'some_label', 'change_number': 123 } } client._logger = mocker.Mock() client._send_impression_to_listener = mocker.Mock() assert client.get_treatment('some_key', 'some_feature') == 'on' assert mocker.call( [Impression('some_key', 'some_feature', 'on', 'some_label', 123, None, 1000)] ) in impression_storage.put.mock_calls assert mocker.call('sdk.getTreatment', 5) in telemetry_storage.inc_latency.mock_calls assert client._logger.mock_calls == [] assert mocker.call( Impression('some_key', 'some_feature', 'on', 'some_label', 123, None, 1000), None ) in client._send_impression_to_listener.mock_calls # Test with exception: split_storage.get_change_number.return_value = -1 def _raise(*_): raise Exception('something') client._evaluator.evaluate_treatment.side_effect = _raise assert client.get_treatment('some_key', 'some_feature') == 'control' assert mocker.call( [Impression('some_key', 'some_feature', 'control', 'exception', -1, None, 1000)] ) in impression_storage.put.mock_calls assert len(telemetry_storage.inc_latency.mock_calls) == 2
def test_wrap_impressions(self, mocker): """Test wrap impressions.""" adapter = mocker.Mock(spec=RedisAdapter) metadata = get_metadata({}) storage = RedisImpressionsStorage(adapter, metadata) impressions = [ Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) ] to_validate = [json.dumps({ 'm': { # METADATA PORTION 's': metadata.sdk_version, 'n': metadata.instance_name, 'i': metadata.instance_ip, }, 'i': { # IMPRESSION PORTION 'k': impression.matching_key, 'b': impression.bucketing_key, 'f': impression.feature_name, 't': impression.treatment, 'r': impression.label, 'c': impression.change_number, 'm': impression.time, } }) for impression in impressions] assert storage._wrap_impressions(impressions) == to_validate
def test_normal_operation(self, mocker): """Test that the task works properly under normal circumstances.""" storage = mocker.Mock(spec=ImpressionStorage) impressions = [ Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), Impression('key3', 'split2', 'off', 'l1', 123456, 'b1', 321654), Impression('key4', 'split2', 'on', 'l1', 123456, 'b1', 321654), Impression('key5', 'split3', 'off', 'l1', 123456, 'b1', 321654) ] storage.pop_many.return_value = impressions api = mocker.Mock(spec=ImpressionsAPI) api.flush_impressions.return_value = HttpResponse(200, '') impression_synchronizer = ImpressionSynchronizer(api, storage, 5) task = impressions_sync.ImpressionsSyncTask( impression_synchronizer.synchronize_impressions, 1) task.start() time.sleep(2) assert task.is_running() assert storage.pop_many.mock_calls[0] == mocker.call(5) assert api.flush_impressions.mock_calls[0] == mocker.call(impressions) stop_event = threading.Event() calls_now = len(api.flush_impressions.mock_calls) task.stop(stop_event) stop_event.wait(5) assert stop_event.is_set() assert len(api.flush_impressions.mock_calls) > calls_now
def test_pipelined_recorder(self, mocker): impressions = [ Impression('k1', 'f1', 'on', 'l1', 123, None, None), Impression('k1', 'f2', 'on', 'l1', 123, None, None) ] redis = mocker.Mock(spec=RedisAdapter) impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions event = mocker.Mock(spec=EventStorage) impression = mocker.Mock(spec=ImpressionStorage) recorder = PipelinedRecorder(redis, impmanager, event, impression) recorder.record_treatment_stats(impressions, 1, 'some') assert recorder._impression_storage.put.mock_calls[0][1][0] == impressions
def test_put_pop_impressions(self, mocker): """Test storing and fetching impressions.""" uwsgi = get_uwsgi(True) storage = UWSGIImpressionStorage(uwsgi) impressions = [ Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) ] storage.put(impressions) res = storage.pop_many(10) assert res == impressions
def test_add_impressions(self, mocker): """Test that adding impressions to storage works.""" adapter = mocker.Mock(spec=RedisAdapter) metadata = get_metadata({}) storage = RedisImpressionsStorage(adapter, metadata) impressions = [ Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) ] assert storage.put(impressions) is True to_validate = [json.dumps({ 'm': { # METADATA PORTION 's': metadata.sdk_version, 'n': metadata.instance_name, 'i': metadata.instance_ip, }, 'i': { # IMPRESSION PORTION 'k': impression.matching_key, 'b': impression.bucketing_key, 'f': impression.feature_name, 't': impression.treatment, 'r': impression.label, 'c': impression.change_number, 'm': impression.time, } }) for impression in impressions] assert adapter.rpush.mock_calls == [ mocker.call('SPLITIO.impressions', *to_validate) ] # Assert that if an exception is thrown it's caught and False is returned adapter.reset_mock() def _raise_exc(*_): raise RedisAdapterException('something') adapter.rpush.side_effect = _raise_exc assert storage.put(impressions) is False
def pop_many(self, count): """ Pop the oldest N impressions from storage. :param count: Number of impressions to pop. :type count: int """ with UWSGILock(self._uwsgi, self._LOCK_IMPRESSION_KEY): try: current = json.loads( self._uwsgi.cache_get( self._IMPRESSIONS_KEY, _SPLITIO_IMPRESSIONS_CACHE_NAMESPACE)) except TypeError: return [] self._uwsgi.cache_update(self._IMPRESSIONS_KEY, json.dumps(current[count:]), 0, _SPLITIO_IMPRESSIONS_CACHE_NAMESPACE) return [ Impression(impression['matching_key'], impression['feature_name'], impression['treatment'], impression['label'], impression['change_number'], impression['bucketing_key'], impression['time']) for impression in current[:count] ]
def test_tracking_and_popping(self): """Test adding impressions counts and popping them.""" counter = Counter() utc_now = utctime_ms_reimplement() utc_1_hour_after = utc_now + (3600 * 1000) counter.track([ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now), Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now), Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now) ]) counter.track([ Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now), Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now) ]) counter.track([ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_1_hour_after), Impression('k1', 'f2', 'on', 'l1', 123, None, utc_1_hour_after) ]) assert set(counter.pop_all()) == set([ Counter.CountPerFeature('f1', truncate_time(utc_now), 3), Counter.CountPerFeature('f2', truncate_time(utc_now), 2), Counter.CountPerFeature('f1', truncate_time(utc_1_hour_after), 1), Counter.CountPerFeature('f2', truncate_time(utc_1_hour_after), 1) ]) assert len(counter._data) == 0 assert set(counter.pop_all()) == set()
def test_clear(self): """Test clear method.""" storage = InMemoryImpressionStorage(100) storage.put( [Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) assert storage._impressions.qsize() == 1 storage.clear() assert storage._impressions.qsize() == 0
def test_standard_recorder(self, mocker): impressions = [ Impression('k1', 'f1', 'on', 'l1', 123, None, None), Impression('k1', 'f2', 'on', 'l1', 123, None, None) ] impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions telemetry = mocker.Mock(spec=TelemetryStorage) event = mocker.Mock(spec=EventStorage) impression = mocker.Mock(spec=ImpressionStorage) recorder = StandardRecorder(impmanager, telemetry, event, impression) recorder.record_treatment_stats(impressions, 1, 'some') assert recorder._impression_storage.put.mock_calls[0][1][ 0] == impressions assert recorder._telemetry_storage.inc_latency.mock_calls == [ mocker.call('some', 1) ]
def test_queue_full_hook(self, mocker): """Test queue_full_hook is executed when the queue is full.""" storage = InMemoryImpressionStorage(100) queue_full_hook = mocker.Mock() storage.set_queue_full_hook(queue_full_hook) impressions = [ Impression('key%d' % i, 'feature1', 'on', 'l1', 123456, 'b1', 321654) for i in range(0, 101) ] storage.put(impressions) assert queue_full_hook.mock_calls == mocker.call()
def test_pipelined_recorder(self, mocker): impressions = [ Impression('k1', 'f1', 'on', 'l1', 123, None, None), Impression('k1', 'f2', 'on', 'l1', 123, None, None) ] redis = mocker.Mock(spec=RedisAdapter) impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions telemetry = mocker.Mock(spec=TelemetryPipelinedStorage) event = mocker.Mock(spec=EventStorage) impression = mocker.Mock(spec=ImpressionPipelinedStorage) recorder = PipelinedRecorder(redis, impmanager, telemetry, event, impression) recorder.record_treatment_stats(impressions, 1, 'some') assert recorder._impression_storage.add_impressions_to_pipe.mock_calls[ 0][1][0] == impressions assert recorder._telemetry_storage.add_latency_to_pipe.mock_calls[0][ 1][0] == 'some' assert recorder._telemetry_storage.add_latency_to_pipe.mock_calls[0][ 1][1] == 1
def test_sampled_recorder(self, mocker): impressions = [ Impression('k1', 'f1', 'on', 'l1', 123, None, None), Impression('k1', 'f2', 'on', 'l1', 123, None, None) ] redis = mocker.Mock(spec=RedisAdapter) impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions event = mocker.Mock(spec=EventStorage) impression = mocker.Mock(spec=ImpressionStorage) recorder = PipelinedRecorder(redis, impmanager, event, impression, 0.5) def put(x): return recorder._impression_storage.put.side_effect = put for _ in range(100): recorder.record_treatment_stats(impressions, 1, 'some') print(recorder._impression_storage.put.call_count) assert recorder._impression_storage.put.call_count < 80
def test_add_impressions_to_pipe(self, mocker): """Test that adding impressions to storage works.""" adapter = mocker.Mock(spec=RedisAdapter) metadata = get_metadata({}) storage = RedisImpressionsStorage(adapter, metadata) impressions = [ Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) ] to_validate = [json.dumps({ 'm': { # METADATA PORTION 's': metadata.sdk_version, 'n': metadata.instance_name, 'i': metadata.instance_ip, }, 'i': { # IMPRESSION PORTION 'k': impression.matching_key, 'b': impression.bucketing_key, 'f': impression.feature_name, 't': impression.treatment, 'r': impression.label, 'c': impression.change_number, 'm': impression.time, } }) for impression in impressions] storage.add_impressions_to_pipe(impressions, adapter) assert adapter.rpush.mock_calls == [ mocker.call('SPLITIO.impressions', *to_validate) ]
def _build_impression( # pylint: disable=too-many-arguments self, matching_key, feature_name, treatment, label, change_number, bucketing_key, imp_time): """Build an impression.""" if not self._labels_enabled: label = None return Impression(matching_key=matching_key, feature_name=feature_name, treatment=treatment, label=label, change_number=change_number, bucketing_key=bucketing_key, time=imp_time)
def test_and_set(self, impression): """ Examine an impression to determine and set it's previous time accordingly. :param impression: Impression to track :type impression: splitio.models.impressions.Impression :returns: Impression with populated previous time :rtype: splitio.models.impressions.Impression """ previous_time = self._cache.test_and_set( self._hasher.process(impression), impression.time) return Impression(impression.matching_key, impression.feature_name, impression.treatment, impression.label, impression.change_number, impression.bucketing_key, impression.time, previous_time)
def test_changes_are_reflected(self): """Test that change in any field changes the resulting hash.""" total = set() hasher = Hasher() total.add( hasher.process( Impression('key1', 'feature1', 'on', 'killed', 123, None, 456))) total.add( hasher.process( Impression('key2', 'feature1', 'on', 'killed', 123, None, 456))) total.add( hasher.process( Impression('key1', 'feature2', 'on', 'killed', 123, None, 456))) total.add( hasher.process( Impression('key1', 'feature1', 'off', 'killed', 123, None, 456))) total.add( hasher.process( Impression('key1', 'feature1', 'on', 'not killed', 123, None, 456))) total.add( hasher.process( Impression('key1', 'feature1', 'on', 'killed', 321, None, 456))) assert len(total) == 6 # Re-adding the first-one should not increase the number of different hashes total.add( hasher.process( Impression('key1', 'feature1', 'on', 'killed', 123, None, 456))) assert len(total) == 6
def test_get_treatment_with_config(self, mocker): """Test get_treatment execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) def _get_storage_mock(name): return { 'splits': split_storage, 'segments': segment_storage, 'impressions': impression_storage, 'events': event_storage, }[name] destroyed_property = mocker.PropertyMock() destroyed_property.return_value = False factory = mocker.Mock(spec=SplitFactory) factory._get_storage.side_effect = _get_storage_mock factory._waiting_fork.return_value = False type(factory).destroyed = destroyed_property mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) impmanager = mocker.Mock(spec=ImpressionManager) recorder = StandardRecorder(impmanager, event_storage, impression_storage) client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.evaluate_feature.return_value = { 'treatment': 'on', 'configurations': '{"some_config": True}', 'impression': { 'label': 'some_label', 'change_number': 123 } } _logger = mocker.Mock() client._send_impression_to_listener = mocker.Mock() assert client.get_treatment_with_config( 'some_key', 'some_feature' ) == ('on', '{"some_config": True}') assert mocker.call( [(Impression('some_key', 'some_feature', 'on', 'some_label', 123, None, 1000), None)] ) in impmanager.process_impressions.mock_calls assert _logger.mock_calls == [] # Test with client not ready ready_property = mocker.PropertyMock() ready_property.return_value = False type(factory).ready = ready_property impmanager.process_impressions.reset_mock() assert client.get_treatment_with_config('some_key', 'some_feature', {'some_attribute': 1}) == ('control', None) assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls # Test with exception: ready_property.return_value = True split_storage.get_change_number.return_value = -1 def _raise(*_): raise Exception('something') client._evaluator.evaluate_feature.side_effect = _raise assert client.get_treatment_with_config('some_key', 'some_feature') == ('control', None) assert mocker.call( [(Impression('some_key', 'some_feature', 'control', 'exception', -1, None, 1000), None)] ) in impmanager.process_impressions.mock_calls
def test_post_impressions(self, mocker): """Test impressions posting API call.""" httpclient = mocker.Mock(spec=client.HttpClient) httpclient.post.return_value = client.HttpResponse(200, '') sdk_metadata = SdkMetadata('python-1.2.3', 'some_machine_name', '123.123.123.123') impressions_api = impressions.ImpressionsAPI(httpclient, 'some_api_key', sdk_metadata) response = impressions_api.flush_impressions([ Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654), Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654), Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654), ]) call_made = httpclient.post.mock_calls[0] # validate positional arguments assert call_made[1] == ('events', '/testImpressions/bulk', 'some_api_key') # validate key-value args (headers) assert call_made[2]['extra_headers'] == { 'SplitSDKVersion': 'python-1.2.3', 'SplitSDKMachineIP': '123.123.123.123', 'SplitSDKMachineName': 'some_machine_name' } # validate key-value args (body) assert call_made[2]['body'] == [{ 'testName': 'f1', 'keyImpressions': [ { 'keyName': 'k1', 'bucketingKey': 'b1', 'treatment': 'on', 'label': 'l1', 'time': 321654, 'changeNumber': 123456 }, { 'keyName': 'k3', 'bucketingKey': 'b1', 'treatment': 'on', 'label': 'l1', 'time': 321654, 'changeNumber': 123456 }, ], }, { 'testName': 'f2', 'keyImpressions': [ { 'keyName': 'k2', 'bucketingKey': 'b1', 'treatment': 'off', 'label': 'l1', 'time': 321654, 'changeNumber': 123456 }, ] }] httpclient.reset_mock() def raise_exception(*args, **kwargs): raise client.HttpClientException('some_message') httpclient.post.side_effect = raise_exception with pytest.raises(APIException) as exc_info: response = impressions_api.flush_impressions([ Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654), Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654), Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654), ]) assert exc_info.type == APIException assert exc_info.value.message == 'some_message'
def test_non_standalone_debug_listener(self, mocker): """Test impressions manager in optimized mode with sdk in standalone mode.""" # Mock utc_time function to be able to play with the clock utc_now = truncate_time(utctime_ms_reimplement()) + 1800 * 1000 utc_time_mock = mocker.Mock() utc_time_mock.return_value = utc_now mocker.patch('splitio.util.utctime_ms', new=utc_time_mock) listener = mocker.Mock(spec=ImpressionListenerWrapper) manager = Manager(ImpressionsMode.DEBUG, False, listener) # no listener assert manager._counter is None assert manager._observer is None assert manager._listener is not None # An impression that hasn't happened in the last hour (pt = None) should be tracked imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 3), None), (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now - 3), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 3), Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now - 3) ] # Tracking the same impression a ms later should return the imp imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 2) ] # Tracking a in impression with a different key makes it to the queue imps = manager.process_impressions([ (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 1), None) ]) assert imps == [ Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 1) ] # Advance the perceived clock one hour old_utc = utc_now # save it to compare captured impressions utc_now += 3600 * 1000 utc_time_mock.return_value = utc_now # Track the same impressions but "one hour later" imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1), None), (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1), Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2) ] assert listener.log_impression.mock_calls == [ mocker.call( Impression('k1', 'f1', 'on', 'l1', 123, None, old_utc - 3), None), mocker.call( Impression('k1', 'f2', 'on', 'l1', 123, None, old_utc - 3), None), mocker.call( Impression('k1', 'f1', 'on', 'l1', 123, None, old_utc - 2), None), mocker.call( Impression('k2', 'f1', 'on', 'l1', 123, None, old_utc - 1), None), mocker.call( Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1), None), mocker.call( Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]
def test_previous_time_properly_calculated(self): """Test that the previous time is properly set.""" observer = Observer(5) assert (observer.test_and_set( Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) assert (observer.test_and_set( Impression('key1', 'f1', 'on', 'killed', 123, None, 457)) == Impression('key1', 'f1', 'on', 'killed', 123, None, 457, 456)) # Add 5 new impressions to evict the first one and check that previous time is None again assert (observer.test_and_set( Impression('key2', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key2', 'f1', 'on', 'killed', 123, None, 456)) assert (observer.test_and_set( Impression('key3', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key3', 'f1', 'on', 'killed', 123, None, 456)) assert (observer.test_and_set( Impression('key4', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key4', 'f1', 'on', 'killed', 123, None, 456)) assert (observer.test_and_set( Impression('key5', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key5', 'f1', 'on', 'killed', 123, None, 456)) assert (observer.test_and_set( Impression('key6', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key6', 'f1', 'on', 'killed', 123, None, 456)) # Re-process the first-one assert (observer.test_and_set( Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) == Impression('key1', 'f1', 'on', 'killed', 123, None, 456))
def test_standalone_optimized_listener(self, mocker): """Test impressions manager in optimized mode with sdk in standalone mode.""" # Mock utc_time function to be able to play with the clock utc_now = truncate_time(utctime_ms_reimplement()) + 1800 * 1000 utc_time_mock = mocker.Mock() utc_time_mock.return_value = utc_now mocker.patch('splitio.util.utctime_ms', new=utc_time_mock) listener = mocker.Mock(spec=ImpressionListenerWrapper) manager = Manager(listener=listener) # no listener assert manager._counter is not None assert manager._observer is not None assert manager._listener is not None # An impression that hasn't happened in the last hour (pt = None) should be tracked imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 3), None), (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now - 3), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 3), Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now - 3) ] # Tracking the same impression a ms later should return empty imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]) assert imps == [] # Tracking a in impression with a different key makes it to the queue imps = manager.process_impressions([ (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 1), None) ]) assert imps == [ Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 1) ] # Advance the perceived clock one hour old_utc = utc_now # save it to compare captured impressions utc_now += 3600 * 1000 utc_time_mock.return_value = utc_now # Track the same impressions but "one hour later" imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1), None), (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1, old_utc - 3), Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2, old_utc - 1) ] assert len( manager._observer._cache._data) == 3 # distinct impressions seen assert len( manager._counter._data ) == 3 # 2 distinct features. 1 seen in 2 different timeframes assert set(manager._counter.pop_all()) == set([ Counter.CountPerFeature('f1', truncate_time(old_utc), 3), Counter.CountPerFeature('f2', truncate_time(old_utc), 1), Counter.CountPerFeature('f1', truncate_time(utc_now), 2) ]) assert listener.log_impression.mock_calls == [ mocker.call( Impression('k1', 'f1', 'on', 'l1', 123, None, old_utc - 3), None), mocker.call( Impression('k1', 'f2', 'on', 'l1', 123, None, old_utc - 3), None), mocker.call( Impression('k1', 'f1', 'on', 'l1', 123, None, old_utc - 2, old_utc - 3), None), mocker.call( Impression('k2', 'f1', 'on', 'l1', 123, None, old_utc - 1), None), mocker.call( Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1, old_utc - 3), None), mocker.call( Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2, old_utc - 1), None) ]
def test_non_standalone_optimized(self, mocker): """Test impressions manager in optimized mode with sdk in standalone mode.""" # Mock utc_time function to be able to play with the clock utc_now = truncate_time(utctime_ms_reimplement()) + 1800 * 1000 utc_time_mock = mocker.Mock() utc_time_mock.return_value = utc_now mocker.patch('splitio.util.utctime_ms', new=utc_time_mock) manager = Manager(ImpressionsMode.OPTIMIZED, False) # no listener assert manager._counter is None assert manager._observer is None assert manager._listener is None # An impression that hasn't happened in the last hour (pt = None) should be tracked imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 3), None), (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now - 3), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 3), Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now - 3) ] # Tracking the same impression a ms later should not be empty imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 2) ] # Tracking a in impression with a different key makes it to the queue imps = manager.process_impressions([ (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 1), None) ]) assert imps == [ Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 1) ] # Advance the perceived clock one hour utc_now += 3600 * 1000 utc_time_mock.return_value = utc_now # Track the same impressions but "one hour later" imps = manager.process_impressions([ (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1), None), (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2), None) ]) assert imps == [ Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now - 1), Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now - 2) ]
def test_get_treatments_with_config(self, mocker): """Test get_treatment execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) telemetry_storage = mocker.Mock(spec=TelemetryStorage) def _get_storage_mock(name): return { 'splits': split_storage, 'segments': segment_storage, 'impressions': impression_storage, 'events': event_storage, 'telemetry': telemetry_storage }[name] destroyed_property = mocker.PropertyMock() destroyed_property.return_value = False factory = mocker.Mock(spec=SplitFactory) factory._get_storage.side_effect = _get_storage_mock factory._waiting_fork.return_value = False type(factory).destroyed = destroyed_property mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) impmanager = mocker.Mock(spec=ImpressionManager) recorder = StandardRecorder(impmanager, telemetry_storage, event_storage, impression_storage) client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', 'configurations': '{"color": "red"}', 'impression': { 'label': 'some_label', 'change_number': 123 } } client._evaluator.evaluate_features.return_value = { 'f1': evaluation, 'f2': evaluation } _logger = mocker.Mock() assert client.get_treatments_with_config('key', ['f1', 'f2']) == { 'f1': ('on', '{"color": "red"}'), 'f2': ('on', '{"color": "red"}') } impressions_called = impmanager.process_impressions.mock_calls[0][1][0] assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called assert mocker.call('sdk.getTreatmentsWithConfig', 5) in telemetry_storage.inc_latency.mock_calls assert _logger.mock_calls == [] # Test with client not ready ready_property = mocker.PropertyMock() ready_property.return_value = False type(factory).ready = ready_property impmanager.process_impressions.reset_mock() assert client.get_treatments_with_config('some_key', ['some_feature'], {'some_attribute': 1}) == { 'some_feature': ('control', None) } assert mocker.call([ (Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), { 'some_attribute': 1 }) ]) in impmanager.process_impressions.mock_calls # Test with exception: ready_property.return_value = True split_storage.get_change_number.return_value = -1 def _raise(*_): raise Exception('something') client._evaluator.evaluate_features.side_effect = _raise assert client.get_treatments_with_config('key', ['f1', 'f2']) == { 'f1': ('control', None), 'f2': ('control', None) } assert len(telemetry_storage.inc_latency.mock_calls) == 2
class ImpressionsAPITests(object): """Impressions API test cases.""" impressions = [ Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654), Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654), Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654) ] expectedImpressions = [{ 'testName': 'f1', 'keyImpressions': [ { 'keyName': 'k1', 'bucketingKey': 'b1', 'treatment': 'on', 'label': 'l1', 'time': 321654, 'changeNumber': 123456 }, { 'keyName': 'k3', 'bucketingKey': 'b1', 'treatment': 'on', 'label': 'l1', 'time': 321654, 'changeNumber': 123456 }, ], }, { 'testName': 'f2', 'keyImpressions': [ { 'keyName': 'k2', 'bucketingKey': 'b1', 'treatment': 'off', 'label': 'l1', 'time': 321654, 'changeNumber': 123456 }, ] }] def test_post_impressions(self, mocker): """Test impressions posting API call.""" httpclient = mocker.Mock(spec=client.HttpClient) httpclient.post.return_value = client.HttpResponse(200, '') cfg = DEFAULT_CONFIG.copy() cfg.update({ 'IPAddressesEnabled': True, 'machineName': 'some_machine_name', 'machineIp': '123.123.123.123' }) sdk_metadata = get_metadata(cfg) impressions_api = impressions.ImpressionsAPI(httpclient, 'some_api_key', sdk_metadata) response = impressions_api.flush_impressions(self.impressions) call_made = httpclient.post.mock_calls[0] # validate positional arguments assert call_made[1] == ('events', '/testImpressions/bulk', 'some_api_key') # validate key-value args (headers) assert call_made[2]['extra_headers'] == { 'SplitSDKVersion': 'python-%s' % __version__, 'SplitSDKMachineIP': '123.123.123.123', 'SplitSDKMachineName': 'some_machine_name' } # validate key-value args (body) assert call_made[2]['body'] == self.expectedImpressions httpclient.reset_mock() def raise_exception(*args, **kwargs): raise client.HttpClientException('some_message') httpclient.post.side_effect = raise_exception with pytest.raises(APIException) as exc_info: response = impressions_api.flush_impressions(self.impressions) assert exc_info.type == APIException assert exc_info.value.message == 'some_message' def test_post_impressions_ip_address_disabled(self, mocker): """Test impressions posting API call.""" httpclient = mocker.Mock(spec=client.HttpClient) httpclient.post.return_value = client.HttpResponse(200, '') cfg = DEFAULT_CONFIG.copy() cfg.update({'IPAddressesEnabled': False}) sdk_metadata = get_metadata(cfg) impressions_api = impressions.ImpressionsAPI(httpclient, 'some_api_key', sdk_metadata) response = impressions_api.flush_impressions(self.impressions) call_made = httpclient.post.mock_calls[0] # validate positional arguments assert call_made[1] == ('events', '/testImpressions/bulk', 'some_api_key') # validate key-value args (headers) assert call_made[2]['extra_headers'] == { 'SplitSDKVersion': 'python-%s' % __version__, } # validate key-value args (body) assert call_made[2]['body'] == self.expectedImpressions
def test_push_pop_impressions(self): """Test pushing and retrieving impressions.""" storage = InMemoryImpressionStorage(100) storage.put( [Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) storage.put( [Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) storage.put( [Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) # Assert impressions are retrieved in the same order they are inserted. assert storage.pop_many(1) == [ Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ] assert storage.pop_many(1) == [ Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ] assert storage.pop_many(1) == [ Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ] # Assert inserting multiple impressions at once works and maintains order. impressions = [ Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654), Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654), Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ] assert storage.put(impressions) # Assert impressions are retrieved in the same order they are inserted. assert storage.pop_many(1) == [ Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ] assert storage.pop_many(1) == [ Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ] assert storage.pop_many(1) == [ Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) ]
class ImpressionsAPITests(object): """Impressions API test cases.""" impressions = [ Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654), Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654), Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654) ] expectedImpressions = [{ 'f': 'f1', 'i': [ { 'k': 'k1', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None }, { 'k': 'k3', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None }, ], }, { 'f': 'f2', 'i': [ { 'k': 'k2', 'b': 'b1', 't': 'off', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None }, ] }] counters = [ Counter.CountPerFeature('f1', 123, 2), Counter.CountPerFeature('f2', 123, 123), Counter.CountPerFeature('f1', 456, 111), Counter.CountPerFeature('f2', 456, 222) ] expected_counters = { 'pf': [ { 'f': 'f1', 'm': 123, 'rc': 2 }, { 'f': 'f2', 'm': 123, 'rc': 123 }, { 'f': 'f1', 'm': 456, 'rc': 111 }, { 'f': 'f2', 'm': 456, 'rc': 222 }, ] } def test_post_impressions(self, mocker): """Test impressions posting API call.""" httpclient = mocker.Mock(spec=client.HttpClient) httpclient.post.return_value = client.HttpResponse(200, '') cfg = DEFAULT_CONFIG.copy() cfg.update({ 'IPAddressesEnabled': True, 'machineName': 'some_machine_name', 'machineIp': '123.123.123.123' }) sdk_metadata = get_metadata(cfg) impressions_api = impressions.ImpressionsAPI(httpclient, 'some_api_key', sdk_metadata) response = impressions_api.flush_impressions(self.impressions) call_made = httpclient.post.mock_calls[0] # validate positional arguments assert call_made[1] == ('events', '/testImpressions/bulk', 'some_api_key') # validate key-value args (headers) assert call_made[2]['extra_headers'] == { 'SplitSDKVersion': 'python-%s' % __version__, 'SplitSDKMachineIP': '123.123.123.123', 'SplitSDKMachineName': 'some_machine_name', 'SplitSDKImpressionsMode': 'OPTIMIZED' } # validate key-value args (body) assert call_made[2]['body'] == self.expectedImpressions httpclient.reset_mock() def raise_exception(*args, **kwargs): raise client.HttpClientException('some_message') httpclient.post.side_effect = raise_exception with pytest.raises(APIException) as exc_info: response = impressions_api.flush_impressions(self.impressions) assert exc_info.type == APIException assert exc_info.value.message == 'some_message' def test_post_impressions_ip_address_disabled(self, mocker): """Test impressions posting API call.""" httpclient = mocker.Mock(spec=client.HttpClient) httpclient.post.return_value = client.HttpResponse(200, '') cfg = DEFAULT_CONFIG.copy() cfg.update({'IPAddressesEnabled': False}) sdk_metadata = get_metadata(cfg) impressions_api = impressions.ImpressionsAPI(httpclient, 'some_api_key', sdk_metadata, ImpressionsMode.DEBUG) response = impressions_api.flush_impressions(self.impressions) call_made = httpclient.post.mock_calls[0] # validate positional arguments assert call_made[1] == ('events', '/testImpressions/bulk', 'some_api_key') # validate key-value args (headers) assert call_made[2]['extra_headers'] == { 'SplitSDKVersion': 'python-%s' % __version__, 'SplitSDKImpressionsMode': 'DEBUG' } # validate key-value args (body) assert call_made[2]['body'] == self.expectedImpressions def test_post_counters(self, mocker): """Test impressions posting API call.""" httpclient = mocker.Mock(spec=client.HttpClient) httpclient.post.return_value = client.HttpResponse(200, '') cfg = DEFAULT_CONFIG.copy() cfg.update({ 'IPAddressesEnabled': True, 'machineName': 'some_machine_name', 'machineIp': '123.123.123.123' }) sdk_metadata = get_metadata(cfg) impressions_api = impressions.ImpressionsAPI(httpclient, 'some_api_key', sdk_metadata) response = impressions_api.flush_counters(self.counters) call_made = httpclient.post.mock_calls[0] # validate positional arguments assert call_made[1] == ('events', '/testImpressions/count', 'some_api_key') # validate key-value args (headers) assert call_made[2]['extra_headers'] == { 'SplitSDKVersion': 'python-%s' % __version__, 'SplitSDKMachineIP': '123.123.123.123', 'SplitSDKMachineName': 'some_machine_name', 'SplitSDKImpressionsMode': 'OPTIMIZED' } # validate key-value args (body) assert call_made[2]['body'] == self.expected_counters httpclient.reset_mock() def raise_exception(*args, **kwargs): raise client.HttpClientException('some_message') httpclient.post.side_effect = raise_exception with pytest.raises(APIException) as exc_info: response = impressions_api.flush_counters(self.counters) assert exc_info.type == APIException assert exc_info.value.message == 'some_message'