Take DBAAS multi-channel publishing Redis modules into use
[ric-plt/sdlpy.git] / ricsdl-package / tests / backend / test_redis.py
1 # Copyright (c) 2019 AT&T Intellectual Property.
2 # Copyright (c) 2018-2019 Nokia.
3 #
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
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
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.
15
16 #
17 # This source code is part of the near-RT RIC (RAN Intelligent Controller)
18 # platform project (RICP).
19 #
20
21
22 from unittest.mock import patch, Mock
23 import pytest
24 from redis import exceptions as redis_exceptions
25 import ricsdl.backend
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
30
31
32 @pytest.fixture()
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'}
38     request.cls.dm_redis_flat = ['{some-ns},a', b'1', '{some-ns},b', b'2']
39     request.cls.key = 'a'
40     request.cls.key_redis = '{some-ns},a'
41     request.cls.keys = ['a', 'b']
42     request.cls.keys_redis = ['{some-ns},a', '{some-ns},b']
43     request.cls.data = b'123'
44     request.cls.old_data = b'1'
45     request.cls.new_data = b'3'
46     request.cls.keypattern = r'[Aa]bc-\[1\].?-*'
47     request.cls.keypattern_redis = r'{some-ns},[Aa]bc-\[1\].?-*'
48     request.cls.matchedkeys = ['Abc-[1].0-def', 'abc-[1].1-ghi']
49     request.cls.matchedkeys_redis = [b'{some-ns},Abc-[1].0-def',
50                                      b'{some-ns},abc-[1].1-ghi']
51     request.cls.matcheddata_redis = [b'10', b'11']
52     request.cls.matchedkeydata = {'Abc-[1].0-def': b'10',
53                                   'abc-[1].1-ghi': b'11'}
54     request.cls.group = 'some-group'
55     request.cls.group_redis = '{some-ns},some-group'
56     request.cls.groupmembers = set([b'm1', b'm2'])
57     request.cls.groupmember = b'm1'
58     request.cls.channels = ['abs', 'gma']
59     request.cls.channels_and_events = {'abs': ['cbn']}
60     request.cls.channels_and_events_redis = ['{some-ns},abs', 'cbn']
61
62     request.cls.configuration = Mock()
63     mock_conf_params = _Configuration.Params(db_host=None,
64                                              db_port=None,
65                                              db_sentinel_port=None,
66                                              db_sentinel_master_name=None,
67                                              db_type=DbBackendType.REDIS)
68     request.cls.configuration.get_params.return_value = mock_conf_params
69     with patch('ricsdl.backend.redis.Redis') as mock_redis, patch(
70             'ricsdl.backend.redis.PubSub') as mock_pubsub:
71         db = ricsdl.backend.get_backend_instance(request.cls.configuration)
72         request.cls.mock_redis = mock_redis.return_value
73         request.cls.mock_pubsub = mock_pubsub.return_value
74     request.cls.db = db
75
76     yield
77
78
79 @pytest.mark.usefixtures('redis_backend_fixture')
80 class TestRedisBackend:
81     def test_is_connected_function_success(self):
82         self.mock_redis.ping.return_value = True
83         ret = self.db.is_connected()
84         self.mock_redis.ping.assert_called_once()
85         assert ret is True
86
87     def test_is_connected_function_returns_false_if_ping_fails(self):
88         self.mock_redis.ping.return_value = False
89         ret = self.db.is_connected()
90         self.mock_redis.ping.assert_called_once()
91         assert ret is False
92
93     def test_is_connected_function_can_map_redis_exception_to_sdl_exception(self):
94         self.mock_redis.ping.side_effect = redis_exceptions.ResponseError('redis error!')
95         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
96             self.db.is_connected()
97
98     def test_set_function_success(self):
99         self.db.set(self.ns, self.dm)
100         self.mock_redis.mset.assert_called_once_with(self.dm_redis)
101
102     def test_set_function_can_map_redis_exception_to_sdl_exception(self):
103         self.mock_redis.mset.side_effect = redis_exceptions.ResponseError('redis error!')
104         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
105             self.db.set(self.ns, self.dm)
106
107     def test_set_if_function_success(self):
108         self.mock_redis.execute_command.return_value = True
109         ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
110         self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
111                                                                 self.new_data, self.old_data)
112         assert ret is True
113
114     def test_set_if_function_returns_false_if_existing_key_value_not_expected(self):
115         self.mock_redis.execute_command.return_value = False
116         ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
117         self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
118                                                                 self.new_data, self.old_data)
119         assert ret is False
120
121     def test_set_if_function_can_map_redis_exception_to_sdl_exception(self):
122         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
123         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
124             self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
125
126     def test_set_if_not_exists_function_success(self):
127         self.mock_redis.setnx.return_value = True
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)
130         assert ret is True
131
132     def test_set_if_not_exists_function_returns_false_if_key_already_exists(self):
133         self.mock_redis.setnx.return_value = False
134         ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
135         self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
136         assert ret is False
137
138     def test_set_if_not_exists_function_can_map_redis_exception_to_sdl_exception(self):
139         self.mock_redis.setnx.side_effect = redis_exceptions.ResponseError('redis error!')
140         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
141             self.db.set_if_not_exists(self.ns, self.key, self.new_data)
142
143     def test_get_function_success(self):
144         self.mock_redis.mget.return_value = self.dl_redis
145         ret = self.db.get(self.ns, self.keys)
146         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
147         assert ret == self.dm
148
149     def test_get_function_returns_empty_dict_when_no_key_values_exist(self):
150         self.mock_redis.mget.return_value = [None, 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 == dict()
154
155     def test_get_function_returns_dict_only_with_found_key_values_when_some_keys_exist(self):
156         self.mock_redis.mget.return_value = [self.data, None]
157         ret = self.db.get(self.ns, self.keys)
158         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
159         assert ret == {self.key: self.data}
160
161     def test_get_function_can_map_redis_exception_to_sdl_exception(self):
162         self.mock_redis.mget.side_effect = redis_exceptions.ResponseError('redis error!')
163         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
164             self.db.get(self.ns, self.keys)
165
166     def test_find_keys_function_success(self):
167         self.mock_redis.keys.return_value = self.matchedkeys_redis
168         ret = self.db.find_keys(self.ns, self.keypattern)
169         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
170         assert ret == self.matchedkeys
171
172     def test_find_keys_function_returns_empty_list_when_no_matching_keys_found(self):
173         self.mock_redis.keys.return_value = []
174         ret = self.db.find_keys(self.ns, self.keypattern)
175         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
176         assert ret == []
177
178     def test_find_keys_function_can_map_redis_exception_to_sdl_exception(self):
179         self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
180         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
181             self.db.find_keys(self.ns, self.keypattern)
182
183     def test_find_keys_function_can_raise_exception_when_redis_key_convert_to_string_fails(self):
184         # Redis returns an illegal key, which conversion to string fails
185         corrupt_redis_key = b'\x81'
186         self.mock_redis.keys.return_value = [corrupt_redis_key]
187         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
188             self.db.find_keys(self.ns, self.keypattern)
189         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
190         "has no namespace prefix" in str(excinfo.value)
191
192     def test_find_keys_function_can_raise_exception_when_redis_key_is_without_prefix(self):
193         # Redis returns an illegal key, which doesn't have comma separated namespace prefix
194         corrupt_redis_key = 'some-corrupt-key'
195         self.mock_redis.keys.return_value = [f'{corrupt_redis_key}'.encode()]
196         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
197             self.db.find_keys(self.ns, self.keypattern)
198         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
199         "has no namespace prefix" in str(excinfo.value)
200
201     def test_find_and_get_function_success(self):
202         self.mock_redis.keys.return_value = self.matchedkeys_redis
203         self.mock_redis.mget.return_value = self.matcheddata_redis
204         ret = self.db.find_and_get(self.ns, self.keypattern)
205         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
206         self.mock_redis.mget.assert_called_once_with([i.decode() for i in self.matchedkeys_redis])
207         assert ret == self.matchedkeydata
208
209     def test_find_and_get_function_returns_empty_dict_when_no_matching_keys_exist(self):
210         self.mock_redis.keys.return_value = list()
211         ret = self.db.find_and_get(self.ns, self.keypattern)
212         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
213         assert not self.mock_redis.mget.called
214         assert ret == dict()
215
216     def test_find_and_get_function_can_map_redis_exception_to_sdl_exception(self):
217         self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
218         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
219             self.db.find_and_get(self.ns, self.keypattern)
220
221     def test_find_and_get_function_can_raise_exception_when_redis_key_convert_to_string_fails(self):
222         # Redis returns an illegal key, which conversion to string fails
223         corrupt_redis_key = b'\x81'
224         self.mock_redis.keys.return_value = [corrupt_redis_key]
225         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
226             self.db.find_and_get(self.ns, self.keypattern)
227         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
228         "has no namespace prefix" in str(excinfo.value)
229
230     def test_find_and_get_function_can_raise_exception_when_redis_key_is_without_prefix(self):
231         # Redis returns an illegal key, which doesn't have comma separated namespace prefix
232         corrupt_redis_key = 'some-corrupt-key'
233         self.mock_redis.keys.return_value = [f'{corrupt_redis_key}'.encode()]
234         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
235             self.db.find_and_get(self.ns, self.keypattern)
236         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
237         "has no namespace prefix" in str(excinfo.value)
238
239     def test_remove_function_success(self):
240         self.db.remove(self.ns, self.keys)
241         self.mock_redis.delete.assert_called_once_with(*self.keys_redis)
242
243     def test_remove_function_can_map_redis_exception_to_sdl_exception(self):
244         self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
245         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
246             self.db.remove(self.ns, self.keys)
247
248     def test_remove_if_function_success(self):
249         self.mock_redis.execute_command.return_value = True
250         ret = self.db.remove_if(self.ns, self.key, self.new_data)
251         self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
252                                                                 self.new_data)
253         assert ret is True
254
255     def test_remove_if_function_returns_false_if_data_does_not_match(self):
256         self.mock_redis.execute_command.return_value = False
257         ret = self.db.remove_if(self.ns, self.key, self.new_data)
258         self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
259                                                                 self.new_data)
260         assert ret is False
261
262     def test_remove_if_function_can_map_redis_exception_to_sdl_exception(self):
263         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
264         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
265             self.db.remove_if(self.ns, self.key, self.new_data)
266
267     def test_add_member_function_success(self):
268         self.db.add_member(self.ns, self.group, self.groupmembers)
269         self.mock_redis.sadd.assert_called_once_with(self.group_redis, *self.groupmembers)
270
271     def test_add_member_function_can_map_redis_exception_to_sdl_exception(self):
272         self.mock_redis.sadd.side_effect = redis_exceptions.ResponseError('redis error!')
273         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
274             self.db.add_member(self.ns, self.group, self.groupmembers)
275
276     def test_remove_member_function_success(self):
277         self.db.remove_member(self.ns, self.group, self.groupmembers)
278         self.mock_redis.srem.assert_called_once_with(self.group_redis, *self.groupmembers)
279
280     def test_remove_member_function_can_map_redis_exception_to_sdl_exception(self):
281         self.mock_redis.srem.side_effect = redis_exceptions.ResponseError('redis error!')
282         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
283             self.db.remove_member(self.ns, self.group, self.groupmembers)
284
285     def test_remove_group_function_success(self):
286         self.db.remove_group(self.ns, self.group)
287         self.mock_redis.delete.assert_called_once_with(self.group_redis)
288
289     def test_remove_group_function_can_map_redis_exception_to_sdl_exception(self):
290         self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
291         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
292             self.db.remove_group(self.ns, self.group)
293
294     def test_get_members_function_success(self):
295         self.mock_redis.smembers.return_value = self.groupmembers
296         ret = self.db.get_members(self.ns, self.group)
297         self.mock_redis.smembers.assert_called_once_with(self.group_redis)
298         assert ret is self.groupmembers
299
300     def test_get_members_function_can_map_redis_exception_to_sdl_exception(self):
301         self.mock_redis.smembers.side_effect = redis_exceptions.ResponseError('redis error!')
302         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
303             self.db.get_members(self.ns, self.group)
304
305     def test_is_member_function_success(self):
306         self.mock_redis.sismember.return_value = True
307         ret = self.db.is_member(self.ns, self.group, self.groupmember)
308         self.mock_redis.sismember.assert_called_once_with(self.group_redis, self.groupmember)
309         assert ret is True
310
311     def test_is_member_function_can_map_redis_exception_to_sdl_exception(self):
312         self.mock_redis.sismember.side_effect = redis_exceptions.ResponseError('redis error!')
313         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
314             self.db.is_member(self.ns, self.group, self.groupmember)
315
316     def test_group_size_function_success(self):
317         self.mock_redis.scard.return_value = 100
318         ret = self.db.group_size(self.ns, self.group)
319         self.mock_redis.scard.assert_called_once_with(self.group_redis)
320         assert ret == 100
321
322     def test_group_size_function_can_map_redis_exception_to_sdl_exception(self):
323         self.mock_redis.scard.side_effect = redis_exceptions.ResponseError('Some redis error!')
324         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
325             self.db.group_size(self.ns, self.group)
326
327     def test_set_and_publish_success(self):
328         self.db.set_and_publish(self.ns, self.channels_and_events, self.dm)
329         self.mock_redis.execute_command.assert_called_once_with('MSETMPUB', len(self.dm),
330                                                                 len(self.channels_and_events),
331                                                                 *self.dm_redis_flat,
332                                                                 *self.channels_and_events_redis)
333
334     def test_set_and_publish_can_map_redis_exception_to_sdl_exception(self):
335         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
336         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
337             self.db.set_and_publish(self.ns, self.channels_and_events, self.dm)
338
339     def test_set_if_and_publish_success(self):
340         self.mock_redis.execute_command.return_value = b"OK"
341         ret = self.db.set_if_and_publish(self.ns, self.channels_and_events, self.key, self.old_data,
342                                          self.new_data)
343         self.mock_redis.execute_command.assert_called_once_with('SETIEMPUB', self.key_redis,
344                                                                 self.new_data, self.old_data,
345                                                                 *self.channels_and_events_redis)
346         assert ret is True
347
348     def test_set_if_and_publish_returns_false_if_existing_key_value_not_expected(self):
349         self.mock_redis.execute_command.return_value = None
350         ret = self.db.set_if_and_publish(self.ns, self.channels_and_events, self.key, self.old_data,
351                                          self.new_data)
352         self.mock_redis.execute_command.assert_called_once_with('SETIEMPUB', self.key_redis,
353                                                                 self.new_data, self.old_data,
354                                                                 *self.channels_and_events_redis)
355         assert ret is False
356
357     def test_set_if_and_publish_can_map_redis_exception_to_sdl_exception(self):
358         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
359         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
360             self.db.set_if_and_publish(self.ns, self.channels_and_events, self.key, self.old_data,
361                                        self.new_data)
362
363     def test_set_if_not_exists_and_publish_success(self):
364         self.mock_redis.execute_command.return_value = b"OK"
365         ret = self.db.set_if_not_exists_and_publish(self.ns, self.channels_and_events, self.key,
366                                                     self.new_data)
367         self.mock_redis.execute_command.assert_called_once_with('SETNXMPUB', self.key_redis,
368                                                                 self.new_data,
369                                                                 *self.channels_and_events_redis)
370         assert ret is True
371
372     def test_set_if_not_exists_and_publish_returns_false_if_key_already_exists(self):
373         self.mock_redis.execute_command.return_value = None
374         ret = self.db.set_if_not_exists_and_publish(self.ns, self.channels_and_events, self.key,
375                                                     self.new_data)
376         self.mock_redis.execute_command.assert_called_once_with('SETNXMPUB', self.key_redis,
377                                                                 self.new_data,
378                                                                 *self.channels_and_events_redis)
379         assert ret is False
380
381     def set_if_not_exists_and_publish_can_map_redis_exception_to_sdl_exception(self):
382         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
383         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
384             self.db.set_if_not_exists_and_publish(self.ns, self.channels_and_events, self.key,
385                                                   self.new_data)
386
387     def test_remove_and_publish_success(self):
388         self.db.remove_and_publish(self.ns, self.channels_and_events, self.key)
389         self.mock_redis.execute_command.assert_called_once_with('DELMPUB', len(self.key),
390                                                                 len(self.channels_and_events),
391                                                                 self.key_redis,
392                                                                 *self.channels_and_events_redis)
393
394     def test_remove_if_and_publish_success(self):
395         self.mock_redis.execute_command.return_value = 1
396         ret = self.db.remove_if_and_publish(self.ns, self.channels_and_events, self.key,
397                                             self.new_data)
398         self.mock_redis.execute_command.assert_called_once_with('DELIEMPUB', self.key_redis,
399                                                                 self.new_data,
400                                                                 *self.channels_and_events_redis)
401         assert ret is True
402
403     def test_remove_if_and_publish_returns_false_if_data_does_not_match(self):
404         self.mock_redis.execute_command.return_value = 0
405         ret = self.db.remove_if_and_publish(self.ns, self.channels_and_events, self.key,
406                                             self.new_data)
407         self.mock_redis.execute_command.assert_called_once_with('DELIEMPUB', self.key_redis,
408                                                                 self.new_data,
409                                                                 *self.channels_and_events_redis)
410         assert ret is False
411
412     def test_remove_if_and_publish_can_map_redis_exception_to_sdl_exception(self):
413         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
414         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
415             self.db.remove_if_and_publish(self.ns, self.channels_and_events, self.key,
416                                           self.new_data)
417
418     def test_remove_all_and_publish_success(self):
419         self.mock_redis.keys.return_value = ['{some-ns},a']
420         self.db.remove_all_and_publish(self.ns, self.channels_and_events)
421         self.mock_redis.keys.assert_called_once()
422         self.mock_redis.execute_command.assert_called_once_with('DELMPUB', len(self.key),
423                                                                 len(self.channels_and_events),
424                                                                 self.key_redis,
425                                                                 *self.channels_and_events_redis)
426
427     def test_remove_all_and_publish_can_map_redis_exception_to_sdl_exception(self):
428         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
429         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
430             self.db.remove_all_and_publish(self.ns, self.channels_and_events)
431
432     def test_subscribe_channel_success(self):
433         cb = Mock()
434         self.db.subscribe_channel(self.ns, cb, self.channels)
435         for channel in self.channels:
436             self.mock_pubsub.subscribe.assert_any_call(**{f'{{some-ns}},{channel}': cb})
437
438     def test_subscribe_channel_with_thread_success(self):
439         cb = Mock()
440         self.db.pubsub_thread.is_alive = Mock()
441         self.db.pubsub_thread.is_alive.return_value = False
442         self.db._run_in_thread = True
443         self.db.subscribe_channel(self.ns, cb, self.channels)
444         self.mock_pubsub.run_in_thread.assert_called_once_with(daemon=True, sleep_time=0.001)
445
446     def test_subscribe_can_map_redis_exception_to_sdl_exeception(self):
447         self.mock_pubsub.subscribe.side_effect = redis_exceptions.ResponseError('redis error!')
448         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
449             self.db.subscribe_channel(self.ns, Mock(), self.channels)
450
451     def test_unsubscribe_channel_success(self):
452         self.db.unsubscribe_channel(self.ns, [self.channels[0]])
453         self.mock_pubsub.unsubscribe.assert_called_with('{some-ns},abs')
454
455     def test_unsubscribe_channel_can_map_redis_exception_to_sdl_exeception(self):
456         self.mock_pubsub.unsubscribe.side_effect = redis_exceptions.ResponseError('redis error!')
457         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
458             self.db.unsubscribe_channel(self.ns, [self.channels[0]])
459
460     def test_start_event_listener_success(self):
461         self.db.start_event_listener()
462         assert self.db._run_in_thread
463
464     def test_start_event_listener_subscribe_first(self):
465         self.mock_pubsub.run_in_thread.return_value = Mock()
466         self.mock_redis.pubsub_channels.return_value = [b'{some-ns},abs']
467         self.db.subscribe_channel(self.ns, Mock(), self.channels)
468         self.db.start_event_listener()
469         self.mock_pubsub.run_in_thread.assert_called_once_with(daemon=True, sleep_time=0.001)
470
471     def test_start_event_listener_fail(self):
472         self.db.pubsub_thread.is_alive = Mock()
473         self.db.pubsub_thread.is_alive.return_value = True
474         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
475             self.db.start_event_listener()
476
477     def test_handle_events_success(self):
478         self.db.handle_events()
479         self.mock_pubsub.get_message.assert_called_once_with(ignore_subscribe_messages=True)
480
481     def test_handle_events_fail_already_started(self):
482         self.db.pubsub_thread.is_alive = Mock()
483         self.db.pubsub_thread.is_alive.return_value = True
484         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
485             self.db.handle_events()
486
487     def test_handle_events_fail_already_set(self):
488         self.db._run_in_thread = True
489         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
490             self.db.handle_events()
491
492     def test_get_redis_connection_function_success(self):
493         ret = self.db.get_redis_connection()
494         assert ret is self.mock_redis
495
496     def test_redis_backend_object_string_representation(self):
497         str_out = str(self.db)
498         assert str_out is not None
499
500
501 class MockRedisLock:
502     def __init__(self, redis, name, timeout=None, sleep=0.1,
503                  blocking=True, blocking_timeout=None, thread_local=True):
504         self.redis = redis
505         self.name = name
506         self.timeout = timeout
507         self.sleep = sleep
508         self.blocking = blocking
509         self.blocking_timeout = blocking_timeout
510         self.thread_local = bool(thread_local)
511
512
513 @pytest.fixture(scope="module")
514 def mock_redis_lock():
515     def _mock_redis_lock(name, timeout=None, sleep=0.1,
516                          blocking=True, blocking_timeout=None, thread_local=True):
517         return MockRedisLock(name, timeout, sleep, blocking, blocking_timeout, thread_local)
518     return _mock_redis_lock
519
520
521 @pytest.fixture()
522 def redis_backend_lock_fixture(request, mock_redis_lock):
523     request.cls.ns = 'some-ns'
524     request.cls.lockname = 'some-lock-name'
525     request.cls.lockname_redis = '{some-ns},some-lock-name'
526     request.cls.expiration = 10
527     request.cls.retry_interval = 0.1
528     request.cls.retry_timeout = 1
529
530     request.cls.mock_lua_get_validity_time = Mock()
531     request.cls.mock_lua_get_validity_time.return_value = 2000
532
533     request.cls.mock_redis = Mock()
534     request.cls.mock_redis.register_script = Mock()
535     request.cls.mock_redis.register_script.return_value = request.cls.mock_lua_get_validity_time
536
537     mocked_dbbackend = Mock()
538     mocked_dbbackend.get_redis_connection.return_value = request.cls.mock_redis
539
540     request.cls.configuration = Mock()
541     mock_conf_params = _Configuration.Params(db_host=None,
542                                              db_port=None,
543                                              db_sentinel_port=None,
544                                              db_sentinel_master_name=None,
545                                              db_type=DbBackendType.REDIS)
546     request.cls.configuration.get_params.return_value = mock_conf_params
547
548     with patch('ricsdl.backend.redis.Lock') as mock_redis_lock:
549         lock = ricsdl.backend.get_backend_lock_instance(request.cls.configuration,
550                                                         request.cls.ns, request.cls.lockname,
551                                                         request.cls.expiration, mocked_dbbackend)
552         request.cls.mock_redis_lock = mock_redis_lock.return_value
553         request.cls.lock = lock
554     yield
555     RedisBackendLock.lua_get_validity_time = None
556
557
558 @pytest.mark.usefixtures('redis_backend_lock_fixture')
559 class TestRedisBackendLock:
560     def test_acquire_function_success(self):
561         self.mock_redis_lock.acquire.return_value = True
562         ret = self.lock.acquire(self.retry_interval, self.retry_timeout)
563         self.mock_redis_lock.acquire.assert_called_once_with(blocking_timeout=self.retry_timeout)
564         assert ret is True
565
566     def test_acquire_function_returns_false_if_lock_is_not_acquired(self):
567         self.mock_redis_lock.acquire.return_value = False
568         ret = self.lock.acquire(self.retry_interval, self.retry_timeout)
569         self.mock_redis_lock.acquire.assert_called_once_with(blocking_timeout=self.retry_timeout)
570         assert ret is False
571
572     def test_acquire_function_can_map_redis_exception_to_sdl_exception(self):
573         self.mock_redis_lock.acquire.side_effect = redis_exceptions.LockError('redis lock error!')
574         with pytest.raises(ricsdl.exceptions.BackendError):
575             self.lock.acquire(self.retry_interval, self.retry_timeout)
576
577     def test_release_function_success(self):
578         self.lock.release()
579         self.mock_redis_lock.release.assert_called_once()
580
581     def test_release_function_can_map_redis_exception_to_sdl_exception(self):
582         self.mock_redis_lock.release.side_effect = redis_exceptions.LockError('redis lock error!')
583         with pytest.raises(ricsdl.exceptions.BackendError):
584             self.lock.release()
585
586     def test_refresh_function_success(self):
587         self.lock.refresh()
588         self.mock_redis_lock.reacquire.assert_called_once()
589
590     def test_refresh_function_can_map_redis_exception_to_sdl_exception(self):
591         self.mock_redis_lock.reacquire.side_effect = redis_exceptions.LockError('redis lock error!')
592         with pytest.raises(ricsdl.exceptions.BackendError):
593             self.lock.refresh()
594
595     def test_get_validity_time_function_success(self):
596         self.mock_redis_lock.name = self.lockname_redis
597         self.mock_redis_lock.local.token = 123
598
599         ret = self.lock.get_validity_time()
600         self.mock_lua_get_validity_time.assert_called_once_with(
601             keys=[self.lockname_redis], args=[123], client=self.mock_redis)
602         assert ret == 2
603
604     def test_get_validity_time_function_second_fraction_success(self):
605         self.mock_redis_lock.name = self.lockname_redis
606         self.mock_redis_lock.local.token = 123
607         self.mock_lua_get_validity_time.return_value = 234
608
609         ret = self.lock.get_validity_time()
610         self.mock_lua_get_validity_time.assert_called_once_with(
611             keys=[self.lockname_redis], args=[123], client=self.mock_redis)
612         assert ret == 0.234
613
614     def test_get_validity_time_function_can_raise_exception_if_lock_is_unlocked(self):
615         self.mock_redis_lock.name = self.lockname_redis
616         self.mock_redis_lock.local.token = None
617
618         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
619             self.lock.get_validity_time()
620         assert f"Cannot get validity time of an unlocked lock {self.lockname}" in str(excinfo.value)
621
622     def test_get_validity_time_function_can_raise_exception_if_lua_script_fails(self):
623         self.mock_redis_lock.name = self.lockname_redis
624         self.mock_redis_lock.local.token = 123
625         self.mock_lua_get_validity_time.return_value = -10
626
627         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
628             self.lock.get_validity_time()
629         assert f"Getting validity time of a lock {self.lockname} failed with error code: -10" in str(excinfo.value)
630
631     def test_redis_backend_lock_object_string_representation(self):
632         expected_lock_info = {'lock DB type': 'Redis',
633                               'lock namespace': 'some-ns',
634                               'lock name': 'some-lock-name',
635                               'lock status': 'locked'}
636         assert str(self.lock) == str(expected_lock_info)
637
638     def test_redis_backend_lock_object_string_representation_can_catch_redis_exception(self):
639         self.mock_redis_lock.owned.side_effect = redis_exceptions.LockError('redis lock error!')
640         expected_lock_info = {'lock DB type': 'Redis',
641                               'lock namespace': 'some-ns',
642                               'lock name': 'some-lock-name',
643                               'lock status': 'Error: redis lock error!'}
644         assert str(self.lock) == str(expected_lock_info)
645
646
647 def test_redis_response_error_exception_is_mapped_to_rejected_by_backend_sdl_exception():
648     with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
649         with _map_to_sdl_exception():
650             raise redis_exceptions.ResponseError('Some redis error!')
651     assert "SDL backend rejected the request: Some redis error!" in str(excinfo.value)
652
653
654 def test_redis_connection_error_exception_is_mapped_to_not_connected_sdl_exception():
655     with pytest.raises(ricsdl.exceptions.NotConnected) as excinfo:
656         with _map_to_sdl_exception():
657             raise redis_exceptions.ConnectionError('Some redis error!')
658     assert "SDL not connected to backend: Some redis error!" in str(excinfo.value)
659
660
661 def test_rest_redis_exceptions_are_mapped_to_backend_error_sdl_exception():
662     with pytest.raises(ricsdl.exceptions.BackendError) as excinfo:
663         with _map_to_sdl_exception():
664             raise redis_exceptions.RedisError('Some redis error!')
665     assert "SDL backend failed to process the request: Some redis error!" in str(excinfo.value)
666
667
668 def test_system_error_exceptions_are_not_mapped_to_any_sdl_exception():
669     with pytest.raises(SystemExit):
670         with _map_to_sdl_exception():
671             raise SystemExit('Fatal error')
672
673
674 class TestRedisClient:
675     @classmethod
676     def setup_class(cls):
677         cls.pubsub = ricsdl.backend.redis.PubSub(Mock())
678         cls.pubsub.channels = {b'{some-ns},abs': Mock()}
679
680     def test_handle_pubsub_message(self):
681         assert self.pubsub.handle_message([b'message', b'{some-ns},abs', b'cbn']) == ('abs', 'cbn')
682         self.pubsub.channels.get(b'{some-ns},abs').assert_called_once_with('abs', 'cbn')