|
15 | 15 | InMemorySegmentStorage, InMemorySplitStorage, InMemoryTelemetryStorage |
16 | 16 | from splitio.storage.redis import RedisEventsStorage, RedisImpressionsStorage, \ |
17 | 17 | RedisSplitStorage, RedisSegmentStorage, RedisTelemetryStorage |
| 18 | +from splitio.storage.pluggable import PluggableEventsStorage, PluggableImpressionsStorage, PluggableSegmentStorage, \ |
| 19 | + PluggableTelemetryStorage, PluggableSplitStorage |
18 | 20 | from splitio.storage.adapters.redis import build, RedisAdapter |
19 | 21 | from splitio.models import splits, segments |
20 | 22 | from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode |
|
25 | 27 | from splitio.recorder.recorder import StandardRecorder, PipelinedRecorder |
26 | 28 | from splitio.client.config import DEFAULT_CONFIG |
27 | 29 | from splitio.sync.synchronizer import SplitTasks, SplitSynchronizers, Synchronizer |
28 | | -from splitio.sync.manager import Manager |
| 30 | +from splitio.sync.manager import Manager, RedisManager |
| 31 | +from splitio.sync.synchronizer import PluggableSynchronizer |
| 32 | + |
29 | 33 | from tests.integration import splits_json |
| 34 | +from tests.storage.test_pluggable import StorageMockAdapter |
| 35 | + |
| 36 | + |
30 | 37 | class InMemoryIntegrationTests(object): |
31 | 38 | """Inmemory storage-based integration tests.""" |
32 | 39 |
|
@@ -1124,3 +1131,324 @@ def test_localhost_e2e(self): |
1124 | 1131 | event = threading.Event() |
1125 | 1132 | factory.destroy(event) |
1126 | 1133 | event.wait() |
| 1134 | + |
| 1135 | + |
| 1136 | +class PluggableIntegrationTests(object): |
| 1137 | + """Pluggable storage-based integration tests.""" |
| 1138 | + |
| 1139 | + def setup_method(self): |
| 1140 | + """Prepare storages with test data.""" |
| 1141 | + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') |
| 1142 | + self.pluggable_storage_adapter = StorageMockAdapter() |
| 1143 | + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') |
| 1144 | + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') |
| 1145 | + |
| 1146 | + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') |
| 1147 | + telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) |
| 1148 | + telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) |
| 1149 | + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() |
| 1150 | + |
| 1151 | + storages = { |
| 1152 | + 'splits': split_storage, |
| 1153 | + 'segments': segment_storage, |
| 1154 | + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), |
| 1155 | + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), |
| 1156 | + 'telemetry': telemetry_pluggable_storage |
| 1157 | + } |
| 1158 | + |
| 1159 | + impmanager = ImpressionsManager(StrategyDebugMode(), telemetry_runtime_producer) # no listener |
| 1160 | + recorder = StandardRecorder(impmanager, storages['events'], |
| 1161 | + storages['impressions'], storages['telemetry']) |
| 1162 | + |
| 1163 | + self.factory = SplitFactory('some_api_key', |
| 1164 | + storages, |
| 1165 | + True, |
| 1166 | + recorder, |
| 1167 | + RedisManager(PluggableSynchronizer()), |
| 1168 | + sdk_ready_flag=None, |
| 1169 | + telemetry_producer=telemetry_producer, |
| 1170 | + telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), |
| 1171 | + ) # pylint:disable=attribute-defined-outside-init |
| 1172 | + |
| 1173 | + # Adding data to storage |
| 1174 | + split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') |
| 1175 | + with open(split_fn, 'r') as flo: |
| 1176 | + data = json.loads(flo.read()) |
| 1177 | + for split in data['splits']: |
| 1178 | + self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) |
| 1179 | + self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) |
| 1180 | + |
| 1181 | + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') |
| 1182 | + with open(segment_fn, 'r') as flo: |
| 1183 | + data = json.loads(flo.read()) |
| 1184 | + self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) |
| 1185 | + self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) |
| 1186 | + |
| 1187 | + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json') |
| 1188 | + with open(segment_fn, 'r') as flo: |
| 1189 | + data = json.loads(flo.read()) |
| 1190 | + self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) |
| 1191 | + self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) |
| 1192 | + |
| 1193 | + |
| 1194 | + def _validate_last_events(self, client, *to_validate): |
| 1195 | + """Validate the last N impressions are present disregarding the order.""" |
| 1196 | + event_storage = client._factory._get_storage('events') |
| 1197 | + events_raw = [] |
| 1198 | + if self.pluggable_storage_adapter.get(event_storage._events_queue_key) is not None: |
| 1199 | + events_raw = [json.loads(im) for im in self.pluggable_storage_adapter.get(event_storage._events_queue_key)] |
| 1200 | + |
| 1201 | + as_tup_set = set( |
| 1202 | + (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) |
| 1203 | + for i in events_raw |
| 1204 | + ) |
| 1205 | + assert as_tup_set == set(to_validate) |
| 1206 | + |
| 1207 | + def _validate_last_impressions(self, client, *to_validate): |
| 1208 | + """Validate the last N impressions are present disregarding the order.""" |
| 1209 | + imp_storage = client._factory._get_storage('impressions') |
| 1210 | + impressions_raw = [] |
| 1211 | + if self.pluggable_storage_adapter.get(imp_storage._impressions_queue_key) is not None: |
| 1212 | + impressions_raw = [json.loads(im) for im in self.pluggable_storage_adapter.get(imp_storage._impressions_queue_key)] |
| 1213 | + as_tup_set = set( |
| 1214 | + (i['i']['f'], i['i']['k'], i['i']['t']) |
| 1215 | + for i in impressions_raw |
| 1216 | + ) |
| 1217 | + |
| 1218 | + assert as_tup_set == set(to_validate) |
| 1219 | + self.pluggable_storage_adapter.delete(imp_storage._impressions_queue_key) |
| 1220 | + |
| 1221 | + def test_get_treatment(self): |
| 1222 | + """Test client.get_treatment().""" |
| 1223 | + client = self.factory.client() |
| 1224 | + |
| 1225 | + assert client.get_treatment('user1', 'sample_feature') == 'on' |
| 1226 | + self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) |
| 1227 | + |
| 1228 | + assert client.get_treatment('invalidKey', 'sample_feature') == 'off' |
| 1229 | + self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) |
| 1230 | + |
| 1231 | + assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' |
| 1232 | + self._validate_last_impressions(client) |
| 1233 | + |
| 1234 | + # testing a killed feature. No matter what the key, must return default treatment |
| 1235 | + assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' |
| 1236 | + self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) |
| 1237 | + |
| 1238 | + # testing ALL matcher |
| 1239 | + assert client.get_treatment('invalidKey', 'all_feature') == 'on' |
| 1240 | + self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) |
| 1241 | + |
| 1242 | + # testing WHITELIST matcher |
| 1243 | + assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' |
| 1244 | + self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) |
| 1245 | + assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' |
| 1246 | + self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) |
| 1247 | + |
| 1248 | + # testing INVALID matcher |
| 1249 | + assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' |
| 1250 | + self._validate_last_impressions(client) |
| 1251 | + |
| 1252 | + # testing Dependency matcher |
| 1253 | + assert client.get_treatment('somekey', 'dependency_test') == 'off' |
| 1254 | + self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) |
| 1255 | + |
| 1256 | + # testing boolean matcher |
| 1257 | + assert client.get_treatment('True', 'boolean_test') == 'on' |
| 1258 | + self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) |
| 1259 | + |
| 1260 | + # testing regex matcher |
| 1261 | + assert client.get_treatment('abc4', 'regex_test') == 'on' |
| 1262 | + self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) |
| 1263 | + |
| 1264 | + def test_get_treatment_with_config(self): |
| 1265 | + """Test client.get_treatment_with_config().""" |
| 1266 | + client = self.factory.client() |
| 1267 | + |
| 1268 | + result = client.get_treatment_with_config('user1', 'sample_feature') |
| 1269 | + assert result == ('on', '{"size":15,"test":20}') |
| 1270 | + self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) |
| 1271 | + |
| 1272 | + result = client.get_treatment_with_config('invalidKey', 'sample_feature') |
| 1273 | + assert result == ('off', None) |
| 1274 | + self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) |
| 1275 | + |
| 1276 | + result = client.get_treatment_with_config('invalidKey', 'invalid_feature') |
| 1277 | + assert result == ('control', None) |
| 1278 | + self._validate_last_impressions(client) |
| 1279 | + |
| 1280 | + # testing a killed feature. No matter what the key, must return default treatment |
| 1281 | + result = client.get_treatment_with_config('invalidKey', 'killed_feature') |
| 1282 | + assert ('defTreatment', '{"size":15,"defTreatment":true}') == result |
| 1283 | + self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) |
| 1284 | + |
| 1285 | + # testing ALL matcher |
| 1286 | + result = client.get_treatment_with_config('invalidKey', 'all_feature') |
| 1287 | + assert result == ('on', None) |
| 1288 | + self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) |
| 1289 | + |
| 1290 | + def test_get_treatments(self): |
| 1291 | + """Test client.get_treatments().""" |
| 1292 | + client = self.factory.client() |
| 1293 | + |
| 1294 | + result = client.get_treatments('user1', ['sample_feature']) |
| 1295 | + assert len(result) == 1 |
| 1296 | + assert result['sample_feature'] == 'on' |
| 1297 | + self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) |
| 1298 | + |
| 1299 | + result = client.get_treatments('invalidKey', ['sample_feature']) |
| 1300 | + assert len(result) == 1 |
| 1301 | + assert result['sample_feature'] == 'off' |
| 1302 | + self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) |
| 1303 | + |
| 1304 | + result = client.get_treatments('invalidKey', ['invalid_feature']) |
| 1305 | + assert len(result) == 1 |
| 1306 | + assert result['invalid_feature'] == 'control' |
| 1307 | + self._validate_last_impressions(client) |
| 1308 | + |
| 1309 | + # testing a killed feature. No matter what the key, must return default treatment |
| 1310 | + result = client.get_treatments('invalidKey', ['killed_feature']) |
| 1311 | + assert len(result) == 1 |
| 1312 | + assert result['killed_feature'] == 'defTreatment' |
| 1313 | + self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) |
| 1314 | + |
| 1315 | + # testing ALL matcher |
| 1316 | + result = client.get_treatments('invalidKey', ['all_feature']) |
| 1317 | + assert len(result) == 1 |
| 1318 | + assert result['all_feature'] == 'on' |
| 1319 | + self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) |
| 1320 | + |
| 1321 | + # testing multiple splitNames |
| 1322 | + result = client.get_treatments('invalidKey', [ |
| 1323 | + 'all_feature', |
| 1324 | + 'killed_feature', |
| 1325 | + 'invalid_feature', |
| 1326 | + 'sample_feature' |
| 1327 | + ]) |
| 1328 | + assert len(result) == 4 |
| 1329 | + assert result['all_feature'] == 'on' |
| 1330 | + assert result['killed_feature'] == 'defTreatment' |
| 1331 | + assert result['invalid_feature'] == 'control' |
| 1332 | + assert result['sample_feature'] == 'off' |
| 1333 | + self._validate_last_impressions( |
| 1334 | + client, |
| 1335 | + ('all_feature', 'invalidKey', 'on'), |
| 1336 | + ('killed_feature', 'invalidKey', 'defTreatment'), |
| 1337 | + ('sample_feature', 'invalidKey', 'off') |
| 1338 | + ) |
| 1339 | + |
| 1340 | + def test_get_treatments_with_config(self): |
| 1341 | + """Test client.get_treatments_with_config().""" |
| 1342 | + client = self.factory.client() |
| 1343 | + |
| 1344 | + result = client.get_treatments_with_config('user1', ['sample_feature']) |
| 1345 | + assert len(result) == 1 |
| 1346 | + assert result['sample_feature'] == ('on', '{"size":15,"test":20}') |
| 1347 | + self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) |
| 1348 | + |
| 1349 | + result = client.get_treatments_with_config('invalidKey', ['sample_feature']) |
| 1350 | + assert len(result) == 1 |
| 1351 | + assert result['sample_feature'] == ('off', None) |
| 1352 | + self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) |
| 1353 | + |
| 1354 | + result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) |
| 1355 | + assert len(result) == 1 |
| 1356 | + assert result['invalid_feature'] == ('control', None) |
| 1357 | + self._validate_last_impressions(client) |
| 1358 | + |
| 1359 | + # testing a killed feature. No matter what the key, must return default treatment |
| 1360 | + result = client.get_treatments_with_config('invalidKey', ['killed_feature']) |
| 1361 | + assert len(result) == 1 |
| 1362 | + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') |
| 1363 | + self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) |
| 1364 | + |
| 1365 | + # testing ALL matcher |
| 1366 | + result = client.get_treatments_with_config('invalidKey', ['all_feature']) |
| 1367 | + assert len(result) == 1 |
| 1368 | + assert result['all_feature'] == ('on', None) |
| 1369 | + self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) |
| 1370 | + |
| 1371 | + # testing multiple splitNames |
| 1372 | + result = client.get_treatments_with_config('invalidKey', [ |
| 1373 | + 'all_feature', |
| 1374 | + 'killed_feature', |
| 1375 | + 'invalid_feature', |
| 1376 | + 'sample_feature' |
| 1377 | + ]) |
| 1378 | + assert len(result) == 4 |
| 1379 | + assert result['all_feature'] == ('on', None) |
| 1380 | + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') |
| 1381 | + assert result['invalid_feature'] == ('control', None) |
| 1382 | + assert result['sample_feature'] == ('off', None) |
| 1383 | + self._validate_last_impressions( |
| 1384 | + client, |
| 1385 | + ('all_feature', 'invalidKey', 'on'), |
| 1386 | + ('killed_feature', 'invalidKey', 'defTreatment'), |
| 1387 | + ('sample_feature', 'invalidKey', 'off'), |
| 1388 | + ) |
| 1389 | + |
| 1390 | + def test_track(self): |
| 1391 | + """Test client.track().""" |
| 1392 | + client = self.factory.client() |
| 1393 | + assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) |
| 1394 | + assert(not client.track(None, 'user', 'conversion')) |
| 1395 | + assert(not client.track('user1', None, 'conversion')) |
| 1396 | + assert(not client.track('user1', 'user', None)) |
| 1397 | + self._validate_last_events( |
| 1398 | + client, |
| 1399 | + ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") |
| 1400 | + ) |
| 1401 | + |
| 1402 | + def test_manager_methods(self): |
| 1403 | + """Test manager.split/splits.""" |
| 1404 | + try: |
| 1405 | + manager = self.factory.manager() |
| 1406 | + except: |
| 1407 | + pass |
| 1408 | + result = manager.split('all_feature') |
| 1409 | + assert result.name == 'all_feature' |
| 1410 | + assert result.traffic_type is None |
| 1411 | + assert result.killed is False |
| 1412 | + assert len(result.treatments) == 2 |
| 1413 | + assert result.change_number == 123 |
| 1414 | + assert result.configs == {} |
| 1415 | + |
| 1416 | + result = manager.split('killed_feature') |
| 1417 | + assert result.name == 'killed_feature' |
| 1418 | + assert result.traffic_type is None |
| 1419 | + assert result.killed is True |
| 1420 | + assert len(result.treatments) == 2 |
| 1421 | + assert result.change_number == 123 |
| 1422 | + assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' |
| 1423 | + assert result.configs['off'] == '{"size":15,"test":20}' |
| 1424 | + |
| 1425 | + result = manager.split('sample_feature') |
| 1426 | + assert result.name == 'sample_feature' |
| 1427 | + assert result.traffic_type is None |
| 1428 | + assert result.killed is False |
| 1429 | + assert len(result.treatments) == 2 |
| 1430 | + assert result.change_number == 123 |
| 1431 | + assert result.configs['on'] == '{"size":15,"test":20}' |
| 1432 | + |
| 1433 | + assert len(manager.split_names()) == 7 |
| 1434 | + assert len(manager.splits()) == 7 |
| 1435 | + |
| 1436 | + def teardown_method(self): |
| 1437 | + """Clear redis cache.""" |
| 1438 | + keys_to_delete = [ |
| 1439 | + "SPLITIO.segment.human_beigns", |
| 1440 | + "SPLITIO.segment.employees.till", |
| 1441 | + "SPLITIO.split.sample_feature", |
| 1442 | + "SPLITIO.splits.till", |
| 1443 | + "SPLITIO.split.killed_feature", |
| 1444 | + "SPLITIO.split.all_feature", |
| 1445 | + "SPLITIO.split.whitelist_feature", |
| 1446 | + "SPLITIO.segment.employees", |
| 1447 | + "SPLITIO.split.regex_test", |
| 1448 | + "SPLITIO.segment.human_beigns.till", |
| 1449 | + "SPLITIO.split.boolean_test", |
| 1450 | + "SPLITIO.split.dependency_test" |
| 1451 | + ] |
| 1452 | + |
| 1453 | + for key in keys_to_delete: |
| 1454 | + self.pluggable_storage_adapter.delete(key) |
0 commit comments