1 # Copyright (c) 2019 AT&T Intellectual Property.
2 # Copyright (c) 2018-2019 Nokia.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 # This source code is part of the near-RT RIC (RAN Intelligent Controller)
18 # platform project (RICP).
22 from unittest.mock import patch, Mock
24 from redis import exceptions as redis_exceptions
26 from ricsdl.backend.redis import (RedisBackendLock, _map_to_sdl_exception)
27 from ricsdl.configuration import _Configuration
28 from ricsdl.configuration import DbBackendType
29 import ricsdl.exceptions
33 def redis_backend_fixture(request):
34 request.cls.ns = 'some-ns'
35 request.cls.dl_redis = [b'1', b'2']
36 request.cls.dm = {'a': b'1', 'b': b'2'}
37 request.cls.dm_redis = {'{some-ns},a': b'1', '{some-ns},b': b'2'}
39 request.cls.key_redis = '{some-ns},a'
40 request.cls.keys = ['a', 'b']
41 request.cls.keys_redis = ['{some-ns},a', '{some-ns},b']
42 request.cls.data = b'123'
43 request.cls.old_data = b'1'
44 request.cls.new_data = b'3'
45 request.cls.keypattern = r'[Aa]bc-\[1\].?-*'
46 request.cls.keypattern_redis = r'{some-ns},[Aa]bc-\[1\].?-*'
47 request.cls.matchedkeys = ['Abc-[1].0-def', 'abc-[1].1-ghi']
48 request.cls.matchedkeys_redis = [b'{some-ns},Abc-[1].0-def',
49 b'{some-ns},abc-[1].1-ghi']
50 request.cls.matcheddata_redis = [b'10', b'11']
51 request.cls.matchedkeydata = {'Abc-[1].0-def': b'10',
52 'abc-[1].1-ghi': b'11'}
53 request.cls.group = 'some-group'
54 request.cls.group_redis = '{some-ns},some-group'
55 request.cls.groupmembers = set([b'm1', b'm2'])
56 request.cls.groupmember = b'm1'
58 request.cls.configuration = Mock()
59 mock_conf_params = _Configuration.Params(db_host=None,
61 db_sentinel_port=None,
62 db_sentinel_master_name=None,
63 db_type=DbBackendType.REDIS)
64 request.cls.configuration.get_params.return_value = mock_conf_params
65 with patch('ricsdl.backend.redis.Redis') as mock_redis:
66 db = ricsdl.backend.get_backend_instance(request.cls.configuration)
67 request.cls.mock_redis = mock_redis.return_value
73 @pytest.mark.usefixtures('redis_backend_fixture')
74 class TestRedisBackend:
75 def test_is_connected_function_success(self):
76 self.mock_redis.ping.return_value = True
77 ret = self.db.is_connected()
78 self.mock_redis.ping.assert_called_once()
81 def test_is_connected_function_returns_false_if_ping_fails(self):
82 self.mock_redis.ping.return_value = False
83 ret = self.db.is_connected()
84 self.mock_redis.ping.assert_called_once()
87 def test_is_connected_function_can_map_redis_exception_to_sdl_exception(self):
88 self.mock_redis.ping.side_effect = redis_exceptions.ResponseError('redis error!')
89 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
90 self.db.is_connected()
92 def test_set_function_success(self):
93 self.db.set(self.ns, self.dm)
94 self.mock_redis.mset.assert_called_once_with(self.dm_redis)
96 def test_set_function_can_map_redis_exception_to_sdl_exception(self):
97 self.mock_redis.mset.side_effect = redis_exceptions.ResponseError('redis error!')
98 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
99 self.db.set(self.ns, self.dm)
101 def test_set_if_function_success(self):
102 self.mock_redis.execute_command.return_value = True
103 ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
104 self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
105 self.new_data, self.old_data)
108 def test_set_if_function_returns_false_if_existing_key_value_not_expected(self):
109 self.mock_redis.execute_command.return_value = False
110 ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
111 self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
112 self.new_data, self.old_data)
115 def test_set_if_function_can_map_redis_exception_to_sdl_exception(self):
116 self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
117 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
118 self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
120 def test_set_if_not_exists_function_success(self):
121 self.mock_redis.setnx.return_value = True
122 ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
123 self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
126 def test_set_if_not_exists_function_returns_false_if_key_already_exists(self):
127 self.mock_redis.setnx.return_value = False
128 ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
129 self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
132 def test_set_if_not_exists_function_can_map_redis_exception_to_sdl_exception(self):
133 self.mock_redis.setnx.side_effect = redis_exceptions.ResponseError('redis error!')
134 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
135 self.db.set_if_not_exists(self.ns, self.key, self.new_data)
137 def test_get_function_success(self):
138 self.mock_redis.mget.return_value = self.dl_redis
139 ret = self.db.get(self.ns, self.keys)
140 self.mock_redis.mget.assert_called_once_with(self.keys_redis)
141 assert ret == self.dm
143 def test_get_function_returns_empty_dict_when_no_key_values_exist(self):
144 self.mock_redis.mget.return_value = [None, None]
145 ret = self.db.get(self.ns, self.keys)
146 self.mock_redis.mget.assert_called_once_with(self.keys_redis)
149 def test_get_function_returns_dict_only_with_found_key_values_when_some_keys_exist(self):
150 self.mock_redis.mget.return_value = [self.data, None]
151 ret = self.db.get(self.ns, self.keys)
152 self.mock_redis.mget.assert_called_once_with(self.keys_redis)
153 assert ret == {self.key: self.data}
155 def test_get_function_can_map_redis_exception_to_sdl_exception(self):
156 self.mock_redis.mget.side_effect = redis_exceptions.ResponseError('redis error!')
157 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
158 self.db.get(self.ns, self.keys)
160 def test_find_keys_function_success(self):
161 self.mock_redis.keys.return_value = self.matchedkeys_redis
162 ret = self.db.find_keys(self.ns, self.keypattern)
163 self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
164 assert ret == self.matchedkeys
166 def test_find_keys_function_returns_empty_list_when_no_matching_keys_found(self):
167 self.mock_redis.keys.return_value = []
168 ret = self.db.find_keys(self.ns, self.keypattern)
169 self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
172 def test_find_keys_function_can_map_redis_exception_to_sdl_exception(self):
173 self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
174 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
175 self.db.find_keys(self.ns, self.keypattern)
177 def test_find_keys_function_can_raise_exception_when_redis_key_convert_to_string_fails(self):
178 # Redis returns an illegal key, which conversion to string fails
179 corrupt_redis_key = b'\x81'
180 self.mock_redis.keys.return_value = [corrupt_redis_key]
181 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
182 self.db.find_keys(self.ns, self.keypattern)
183 assert f"Namespace {self.ns} key:{corrupt_redis_key} "
184 "has no namespace prefix" in str(excinfo.value)
186 def test_find_keys_function_can_raise_exception_when_redis_key_is_without_prefix(self):
187 # Redis returns an illegal key, which doesn't have comma separated namespace prefix
188 corrupt_redis_key = 'some-corrupt-key'
189 self.mock_redis.keys.return_value = [f'{corrupt_redis_key}'.encode()]
190 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
191 self.db.find_keys(self.ns, self.keypattern)
192 assert f"Namespace {self.ns} key:{corrupt_redis_key} "
193 "has no namespace prefix" in str(excinfo.value)
195 def test_find_and_get_function_success(self):
196 self.mock_redis.keys.return_value = self.matchedkeys_redis
197 self.mock_redis.mget.return_value = self.matcheddata_redis
198 ret = self.db.find_and_get(self.ns, self.keypattern)
199 self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
200 self.mock_redis.mget.assert_called_once_with([i.decode() for i in self.matchedkeys_redis])
201 assert ret == self.matchedkeydata
203 def test_find_and_get_function_returns_empty_dict_when_no_matching_keys_exist(self):
204 self.mock_redis.keys.return_value = list()
205 ret = self.db.find_and_get(self.ns, self.keypattern)
206 self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
207 assert not self.mock_redis.mget.called
210 def test_find_and_get_function_can_map_redis_exception_to_sdl_exception(self):
211 self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
212 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
213 self.db.find_and_get(self.ns, self.keypattern)
215 def test_find_and_get_function_can_raise_exception_when_redis_key_convert_to_string_fails(self):
216 # Redis returns an illegal key, which conversion to string fails
217 corrupt_redis_key = b'\x81'
218 self.mock_redis.keys.return_value = [corrupt_redis_key]
219 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
220 self.db.find_and_get(self.ns, self.keypattern)
221 assert f"Namespace {self.ns} key:{corrupt_redis_key} "
222 "has no namespace prefix" in str(excinfo.value)
224 def test_find_and_get_function_can_raise_exception_when_redis_key_is_without_prefix(self):
225 # Redis returns an illegal key, which doesn't have comma separated namespace prefix
226 corrupt_redis_key = 'some-corrupt-key'
227 self.mock_redis.keys.return_value = [f'{corrupt_redis_key}'.encode()]
228 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
229 self.db.find_and_get(self.ns, self.keypattern)
230 assert f"Namespace {self.ns} key:{corrupt_redis_key} "
231 "has no namespace prefix" in str(excinfo.value)
233 def test_remove_function_success(self):
234 self.db.remove(self.ns, self.keys)
235 self.mock_redis.delete.assert_called_once_with(*self.keys_redis)
237 def test_remove_function_can_map_redis_exception_to_sdl_exception(self):
238 self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
239 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
240 self.db.remove(self.ns, self.keys)
242 def test_remove_if_function_success(self):
243 self.mock_redis.execute_command.return_value = True
244 ret = self.db.remove_if(self.ns, self.key, self.new_data)
245 self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
249 def test_remove_if_function_returns_false_if_data_does_not_match(self):
250 self.mock_redis.execute_command.return_value = False
251 ret = self.db.remove_if(self.ns, self.key, self.new_data)
252 self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
256 def test_remove_if_function_can_map_redis_exception_to_sdl_exception(self):
257 self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
258 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
259 self.db.remove_if(self.ns, self.key, self.new_data)
261 def test_add_member_function_success(self):
262 self.db.add_member(self.ns, self.group, self.groupmembers)
263 self.mock_redis.sadd.assert_called_once_with(self.group_redis, *self.groupmembers)
265 def test_add_member_function_can_map_redis_exception_to_sdl_exception(self):
266 self.mock_redis.sadd.side_effect = redis_exceptions.ResponseError('redis error!')
267 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
268 self.db.add_member(self.ns, self.group, self.groupmembers)
270 def test_remove_member_function_success(self):
271 self.db.remove_member(self.ns, self.group, self.groupmembers)
272 self.mock_redis.srem.assert_called_once_with(self.group_redis, *self.groupmembers)
274 def test_remove_member_function_can_map_redis_exception_to_sdl_exception(self):
275 self.mock_redis.srem.side_effect = redis_exceptions.ResponseError('redis error!')
276 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
277 self.db.remove_member(self.ns, self.group, self.groupmembers)
279 def test_remove_group_function_success(self):
280 self.db.remove_group(self.ns, self.group)
281 self.mock_redis.delete.assert_called_once_with(self.group_redis)
283 def test_remove_group_function_can_map_redis_exception_to_sdl_exception(self):
284 self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
285 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
286 self.db.remove_group(self.ns, self.group)
288 def test_get_members_function_success(self):
289 self.mock_redis.smembers.return_value = self.groupmembers
290 ret = self.db.get_members(self.ns, self.group)
291 self.mock_redis.smembers.assert_called_once_with(self.group_redis)
292 assert ret is self.groupmembers
294 def test_get_members_function_can_map_redis_exception_to_sdl_exception(self):
295 self.mock_redis.smembers.side_effect = redis_exceptions.ResponseError('redis error!')
296 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
297 self.db.get_members(self.ns, self.group)
299 def test_is_member_function_success(self):
300 self.mock_redis.sismember.return_value = True
301 ret = self.db.is_member(self.ns, self.group, self.groupmember)
302 self.mock_redis.sismember.assert_called_once_with(self.group_redis, self.groupmember)
305 def test_is_member_function_can_map_redis_exception_to_sdl_exception(self):
306 self.mock_redis.sismember.side_effect = redis_exceptions.ResponseError('redis error!')
307 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
308 self.db.is_member(self.ns, self.group, self.groupmember)
310 def test_group_size_function_success(self):
311 self.mock_redis.scard.return_value = 100
312 ret = self.db.group_size(self.ns, self.group)
313 self.mock_redis.scard.assert_called_once_with(self.group_redis)
316 def test_group_size_function_can_map_redis_exception_to_sdl_exception(self):
317 self.mock_redis.scard.side_effect = redis_exceptions.ResponseError('Some redis error!')
318 with pytest.raises(ricsdl.exceptions.RejectedByBackend):
319 self.db.group_size(self.ns, self.group)
321 def test_get_redis_connection_function_success(self):
322 ret = self.db.get_redis_connection()
323 assert ret is self.mock_redis
325 def test_redis_backend_object_string_representation(self):
326 str_out = str(self.db)
327 assert str_out is not None
331 def __init__(self, redis, name, timeout=None, sleep=0.1,
332 blocking=True, blocking_timeout=None, thread_local=True):
335 self.timeout = timeout
337 self.blocking = blocking
338 self.blocking_timeout = blocking_timeout
339 self.thread_local = bool(thread_local)
342 @pytest.fixture(scope="module")
343 def mock_redis_lock():
344 def _mock_redis_lock(name, timeout=None, sleep=0.1,
345 blocking=True, blocking_timeout=None, thread_local=True):
346 return MockRedisLock(name, timeout, sleep, blocking, blocking_timeout, thread_local)
347 return _mock_redis_lock
351 def redis_backend_lock_fixture(request, mock_redis_lock):
352 request.cls.ns = 'some-ns'
353 request.cls.lockname = 'some-lock-name'
354 request.cls.lockname_redis = '{some-ns},some-lock-name'
355 request.cls.expiration = 10
356 request.cls.retry_interval = 0.1
357 request.cls.retry_timeout = 1
359 request.cls.mock_lua_get_validity_time = Mock()
360 request.cls.mock_lua_get_validity_time.return_value = 2000
362 request.cls.mock_redis = Mock()
363 request.cls.mock_redis.register_script = Mock()
364 request.cls.mock_redis.register_script.return_value = request.cls.mock_lua_get_validity_time
366 mocked_dbbackend = Mock()
367 mocked_dbbackend.get_redis_connection.return_value = request.cls.mock_redis
369 request.cls.configuration = Mock()
370 mock_conf_params = _Configuration.Params(db_host=None,
372 db_sentinel_port=None,
373 db_sentinel_master_name=None,
374 db_type=DbBackendType.REDIS)
375 request.cls.configuration.get_params.return_value = mock_conf_params
377 with patch('ricsdl.backend.redis.Lock') as mock_redis_lock:
378 lock = ricsdl.backend.get_backend_lock_instance(request.cls.configuration,
379 request.cls.ns, request.cls.lockname,
380 request.cls.expiration, mocked_dbbackend)
381 request.cls.mock_redis_lock = mock_redis_lock.return_value
382 request.cls.lock = lock
384 RedisBackendLock.lua_get_validity_time = None
387 @pytest.mark.usefixtures('redis_backend_lock_fixture')
388 class TestRedisBackendLock:
389 def test_acquire_function_success(self):
390 self.mock_redis_lock.acquire.return_value = True
391 ret = self.lock.acquire(self.retry_interval, self.retry_timeout)
392 self.mock_redis_lock.acquire.assert_called_once_with(blocking_timeout=self.retry_timeout)
395 def test_acquire_function_returns_false_if_lock_is_not_acquired(self):
396 self.mock_redis_lock.acquire.return_value = False
397 ret = self.lock.acquire(self.retry_interval, self.retry_timeout)
398 self.mock_redis_lock.acquire.assert_called_once_with(blocking_timeout=self.retry_timeout)
401 def test_acquire_function_can_map_redis_exception_to_sdl_exception(self):
402 self.mock_redis_lock.acquire.side_effect = redis_exceptions.LockError('redis lock error!')
403 with pytest.raises(ricsdl.exceptions.BackendError):
404 self.lock.acquire(self.retry_interval, self.retry_timeout)
406 def test_release_function_success(self):
408 self.mock_redis_lock.release.assert_called_once()
410 def test_release_function_can_map_redis_exception_to_sdl_exception(self):
411 self.mock_redis_lock.release.side_effect = redis_exceptions.LockError('redis lock error!')
412 with pytest.raises(ricsdl.exceptions.BackendError):
415 def test_refresh_function_success(self):
417 self.mock_redis_lock.reacquire.assert_called_once()
419 def test_refresh_function_can_map_redis_exception_to_sdl_exception(self):
420 self.mock_redis_lock.reacquire.side_effect = redis_exceptions.LockError('redis lock error!')
421 with pytest.raises(ricsdl.exceptions.BackendError):
424 def test_get_validity_time_function_success(self):
425 self.mock_redis_lock.name = self.lockname_redis
426 self.mock_redis_lock.local.token = 123
428 ret = self.lock.get_validity_time()
429 self.mock_lua_get_validity_time.assert_called_once_with(
430 keys=[self.lockname_redis], args=[123], client=self.mock_redis)
433 def test_get_validity_time_function_second_fraction_success(self):
434 self.mock_redis_lock.name = self.lockname_redis
435 self.mock_redis_lock.local.token = 123
436 self.mock_lua_get_validity_time.return_value = 234
438 ret = self.lock.get_validity_time()
439 self.mock_lua_get_validity_time.assert_called_once_with(
440 keys=[self.lockname_redis], args=[123], client=self.mock_redis)
443 def test_get_validity_time_function_can_raise_exception_if_lock_is_unlocked(self):
444 self.mock_redis_lock.name = self.lockname_redis
445 self.mock_redis_lock.local.token = None
447 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
448 self.lock.get_validity_time()
449 assert f"Cannot get validity time of an unlocked lock {self.lockname}" in str(excinfo.value)
451 def test_get_validity_time_function_can_raise_exception_if_lua_script_fails(self):
452 self.mock_redis_lock.name = self.lockname_redis
453 self.mock_redis_lock.local.token = 123
454 self.mock_lua_get_validity_time.return_value = -10
456 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
457 self.lock.get_validity_time()
458 assert f"Getting validity time of a lock {self.lockname} failed with error code: -10" in str(excinfo.value)
460 def test_redis_backend_lock_object_string_representation(self):
461 expected_lock_info = {'lock DB type': 'Redis',
462 'lock namespace': 'some-ns',
463 'lock name': 'some-lock-name',
464 'lock status': 'locked'}
465 assert str(self.lock) == str(expected_lock_info)
467 def test_redis_backend_lock_object_string_representation_can_catch_redis_exception(self):
468 self.mock_redis_lock.owned.side_effect = redis_exceptions.LockError('redis lock error!')
469 expected_lock_info = {'lock DB type': 'Redis',
470 'lock namespace': 'some-ns',
471 'lock name': 'some-lock-name',
472 'lock status': 'Error: redis lock error!'}
473 assert str(self.lock) == str(expected_lock_info)
476 def test_redis_response_error_exception_is_mapped_to_rejected_by_backend_sdl_exception():
477 with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
478 with _map_to_sdl_exception():
479 raise redis_exceptions.ResponseError('Some redis error!')
480 assert "SDL backend rejected the request: Some redis error!" in str(excinfo.value)
483 def test_redis_connection_error_exception_is_mapped_to_not_connected_sdl_exception():
484 with pytest.raises(ricsdl.exceptions.NotConnected) as excinfo:
485 with _map_to_sdl_exception():
486 raise redis_exceptions.ConnectionError('Some redis error!')
487 assert "SDL not connected to backend: Some redis error!" in str(excinfo.value)
490 def test_rest_redis_exceptions_are_mapped_to_backend_error_sdl_exception():
491 with pytest.raises(ricsdl.exceptions.BackendError) as excinfo:
492 with _map_to_sdl_exception():
493 raise redis_exceptions.RedisError('Some redis error!')
494 assert "SDL backend failed to process the request: Some redis error!" in str(excinfo.value)
497 def test_system_error_exceptions_are_not_mapped_to_any_sdl_exception():
498 with pytest.raises(SystemExit):
499 with _map_to_sdl_exception():
500 raise SystemExit('Fatal error')