nucliadb-utils 4.0.3.post572__py3-none-any.whl → 4.0.3.post575__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nucliadb_utils/tests/{conftest.py → fixtures.py} +1 -9
- {nucliadb_utils-4.0.3.post572.dist-info → nucliadb_utils-4.0.3.post575.dist-info}/METADATA +3 -3
- {nucliadb_utils-4.0.3.post572.dist-info → nucliadb_utils-4.0.3.post575.dist-info}/RECORD +6 -21
- nucliadb_utils/tests/unit/__init__.py +0 -18
- nucliadb_utils/tests/unit/storages/__init__.py +0 -18
- nucliadb_utils/tests/unit/storages/test_aws.py +0 -60
- nucliadb_utils/tests/unit/storages/test_gcs.py +0 -119
- nucliadb_utils/tests/unit/storages/test_pg.py +0 -561
- nucliadb_utils/tests/unit/storages/test_storage.py +0 -228
- nucliadb_utils/tests/unit/test_asyncio_utils.py +0 -72
- nucliadb_utils/tests/unit/test_authentication.py +0 -159
- nucliadb_utils/tests/unit/test_helpers.py +0 -45
- nucliadb_utils/tests/unit/test_nats.py +0 -154
- nucliadb_utils/tests/unit/test_run.py +0 -57
- nucliadb_utils/tests/unit/test_signals.py +0 -81
- nucliadb_utils/tests/unit/test_tests.py +0 -27
- nucliadb_utils/tests/unit/test_transaction.py +0 -119
- nucliadb_utils/tests/unit/test_utilities.py +0 -173
- {nucliadb_utils-4.0.3.post572.dist-info → nucliadb_utils-4.0.3.post575.dist-info}/WHEEL +0 -0
- {nucliadb_utils-4.0.3.post572.dist-info → nucliadb_utils-4.0.3.post575.dist-info}/top_level.txt +0 -0
- {nucliadb_utils-4.0.3.post572.dist-info → nucliadb_utils-4.0.3.post575.dist-info}/zip-safe +0 -0
@@ -1,228 +0,0 @@
|
|
1
|
-
# Copyright (C) 2021 Bosutech XXI S.L.
|
2
|
-
#
|
3
|
-
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
4
|
-
# For commercial licensing, contact us at info@nuclia.com.
|
5
|
-
#
|
6
|
-
# AGPL:
|
7
|
-
# This program is free software: you can redistribute it and/or modify
|
8
|
-
# it under the terms of the GNU Affero General Public License as
|
9
|
-
# published by the Free Software Foundation, either version 3 of the
|
10
|
-
# License, or (at your option) any later version.
|
11
|
-
#
|
12
|
-
# This program is distributed in the hope that it will be useful,
|
13
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
-
# GNU Affero General Public License for more details.
|
16
|
-
#
|
17
|
-
# You should have received a copy of the GNU Affero General Public License
|
18
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
-
|
20
|
-
from math import ceil
|
21
|
-
from unittest.mock import AsyncMock, MagicMock
|
22
|
-
|
23
|
-
import pytest
|
24
|
-
from nucliadb_protos.noderesources_pb2 import Resource as BrainResource
|
25
|
-
from nucliadb_protos.noderesources_pb2 import ResourceID
|
26
|
-
from nucliadb_protos.nodewriter_pb2 import IndexMessage
|
27
|
-
from nucliadb_protos.resources_pb2 import CloudFile
|
28
|
-
|
29
|
-
from nucliadb_utils.storages.local import LocalStorageField
|
30
|
-
from nucliadb_utils.storages.storage import (
|
31
|
-
ObjectInfo,
|
32
|
-
Storage,
|
33
|
-
StorageField,
|
34
|
-
iter_and_add_size,
|
35
|
-
iter_in_chunk_size,
|
36
|
-
)
|
37
|
-
|
38
|
-
|
39
|
-
class TestStorageField:
|
40
|
-
@pytest.fixture
|
41
|
-
def storage(self):
|
42
|
-
yield AsyncMock(source=0)
|
43
|
-
|
44
|
-
@pytest.fixture
|
45
|
-
def field(self):
|
46
|
-
yield MagicMock(uri="uri")
|
47
|
-
|
48
|
-
@pytest.fixture
|
49
|
-
def storage_field(self, storage, field):
|
50
|
-
yield LocalStorageField(storage, "bucket", "fullkey", field)
|
51
|
-
|
52
|
-
@pytest.mark.asyncio
|
53
|
-
async def test_delete(self, storage_field: StorageField, storage):
|
54
|
-
await storage_field.delete()
|
55
|
-
storage.delete_upload.assert_called_once_with("uri", "bucket")
|
56
|
-
|
57
|
-
|
58
|
-
class StorageTest(Storage):
|
59
|
-
def __init__(self):
|
60
|
-
self.source = 0
|
61
|
-
self.field_klass = lambda: MagicMock()
|
62
|
-
self.deadletter_bucket = "deadletter_bucket"
|
63
|
-
self.indexing_bucket = "indexing_bucket"
|
64
|
-
self.delete_upload = AsyncMock()
|
65
|
-
self.uploadbytes = AsyncMock()
|
66
|
-
self.move = AsyncMock()
|
67
|
-
|
68
|
-
def get_bucket_name(self, kbid):
|
69
|
-
return "bucket"
|
70
|
-
|
71
|
-
async def iterate_objects(self, bucket_name, prefix):
|
72
|
-
yield ObjectInfo(name="uri")
|
73
|
-
|
74
|
-
async def download(self, bucket_name, uri):
|
75
|
-
br = BrainResource(labels=["label"])
|
76
|
-
yield br.SerializeToString()
|
77
|
-
|
78
|
-
async def create_kb(self, kbid):
|
79
|
-
return True
|
80
|
-
|
81
|
-
async def delete_kb(self, kbid):
|
82
|
-
return True
|
83
|
-
|
84
|
-
async def delete_upload(self, uri, bucket):
|
85
|
-
return True
|
86
|
-
|
87
|
-
async def initialize(self) -> None:
|
88
|
-
pass
|
89
|
-
|
90
|
-
async def finalize(self) -> None:
|
91
|
-
pass
|
92
|
-
|
93
|
-
async def schedule_delete_kb(self, kbid: str) -> bool:
|
94
|
-
return True
|
95
|
-
|
96
|
-
|
97
|
-
class TestStorage:
|
98
|
-
@pytest.fixture
|
99
|
-
def storage(self):
|
100
|
-
yield StorageTest()
|
101
|
-
|
102
|
-
@pytest.mark.asyncio
|
103
|
-
async def test_delete_resource(self, storage: StorageTest):
|
104
|
-
await storage.delete_resource("bucket", "uri")
|
105
|
-
|
106
|
-
storage.delete_upload.assert_called_once_with("uri", "bucket")
|
107
|
-
|
108
|
-
@pytest.mark.asyncio
|
109
|
-
async def test_indexing(self, storage: StorageTest):
|
110
|
-
msg = BrainResource(resource=ResourceID(uuid="uuid"))
|
111
|
-
await storage.indexing(msg, 1, "1", "kb", "shard")
|
112
|
-
|
113
|
-
storage.uploadbytes.assert_called_once_with(
|
114
|
-
"indexing_bucket", "index/kb/shard/uuid/1", msg.SerializeToString()
|
115
|
-
)
|
116
|
-
|
117
|
-
@pytest.mark.asyncio
|
118
|
-
async def test_reindexing(self, storage: StorageTest):
|
119
|
-
msg = BrainResource(resource=ResourceID(uuid="uuid"))
|
120
|
-
await storage.reindexing(msg, "reindex_id", "1", "kb", "shard")
|
121
|
-
|
122
|
-
storage.uploadbytes.assert_called_once_with(
|
123
|
-
"indexing_bucket", "index/kb/shard/uuid/reindex_id", msg.SerializeToString()
|
124
|
-
)
|
125
|
-
|
126
|
-
@pytest.mark.asyncio
|
127
|
-
async def test_get_indexing(self, storage: StorageTest):
|
128
|
-
im = IndexMessage()
|
129
|
-
im.node = "node"
|
130
|
-
im.shard = "shard"
|
131
|
-
im.txid = 0
|
132
|
-
assert isinstance(await storage.get_indexing(im), BrainResource)
|
133
|
-
|
134
|
-
@pytest.mark.asyncio
|
135
|
-
async def test_get_indexing_storage_key(self, storage: StorageTest):
|
136
|
-
im = IndexMessage()
|
137
|
-
im.node = "node"
|
138
|
-
im.shard = "shard"
|
139
|
-
im.txid = 0
|
140
|
-
im.storage_key = "index/kb/uuid/1"
|
141
|
-
assert isinstance(await storage.get_indexing(im), BrainResource)
|
142
|
-
|
143
|
-
@pytest.mark.asyncio
|
144
|
-
async def test_delete_indexing(self, storage: StorageTest):
|
145
|
-
im = IndexMessage()
|
146
|
-
im.node = "node"
|
147
|
-
im.txid = 0
|
148
|
-
im.storage_key = "index/kb/uuid/1"
|
149
|
-
await storage.delete_indexing(
|
150
|
-
resource_uid="resource_uid", txid=1, kb="kb", logical_shard="logical_shard"
|
151
|
-
)
|
152
|
-
|
153
|
-
storage.uploadbytes.assert_called_once()
|
154
|
-
|
155
|
-
@pytest.mark.asyncio
|
156
|
-
async def test_download_pb(self, storage: StorageTest):
|
157
|
-
assert isinstance(
|
158
|
-
await storage.download_pb(
|
159
|
-
LocalStorageField(storage, "bucket", "fullkey"), BrainResource
|
160
|
-
),
|
161
|
-
BrainResource,
|
162
|
-
)
|
163
|
-
|
164
|
-
@pytest.mark.asyncio
|
165
|
-
async def test_indexing_bucket_none_attributeerrror(self, storage: StorageTest):
|
166
|
-
storage.indexing_bucket = None
|
167
|
-
msg = BrainResource()
|
168
|
-
im = IndexMessage(node="node", shard="shard", txid=0)
|
169
|
-
|
170
|
-
with pytest.raises(AttributeError):
|
171
|
-
await storage.indexing(msg, 1, "1", "kb", "shard")
|
172
|
-
|
173
|
-
with pytest.raises(AttributeError):
|
174
|
-
await storage.reindexing(msg, "reindex_id", "1", "kb", "shard")
|
175
|
-
|
176
|
-
with pytest.raises(AttributeError):
|
177
|
-
await storage.get_indexing(im)
|
178
|
-
|
179
|
-
with pytest.raises(AttributeError):
|
180
|
-
await storage.delete_indexing(
|
181
|
-
resource_uid="resource_uid",
|
182
|
-
txid=1,
|
183
|
-
kb="kb",
|
184
|
-
logical_shard="logical_shard",
|
185
|
-
)
|
186
|
-
|
187
|
-
|
188
|
-
async def testiter_and_add_size():
|
189
|
-
cf = CloudFile()
|
190
|
-
|
191
|
-
async def iter():
|
192
|
-
yield b"foo"
|
193
|
-
yield b"bar"
|
194
|
-
|
195
|
-
cf.size = 0
|
196
|
-
async for _ in iter_and_add_size(iter(), cf):
|
197
|
-
pass
|
198
|
-
|
199
|
-
assert cf.size == 6
|
200
|
-
|
201
|
-
|
202
|
-
async def test_iter_in_chunk_size():
|
203
|
-
async def iterable(total_size, *, chunk_size=1):
|
204
|
-
data = b"0" * total_size
|
205
|
-
for i in range(ceil(total_size / chunk_size)):
|
206
|
-
chunk = data[i * chunk_size : (i + 1) * chunk_size]
|
207
|
-
yield chunk
|
208
|
-
|
209
|
-
chunks = [chunk async for chunk in iter_in_chunk_size(iterable(10), chunk_size=4)]
|
210
|
-
assert len(chunks) == 3
|
211
|
-
assert len(chunks[0]) == 4
|
212
|
-
assert len(chunks[1]) == 4
|
213
|
-
assert len(chunks[2]) == 2
|
214
|
-
|
215
|
-
chunks = [chunk async for chunk in iter_in_chunk_size(iterable(0), chunk_size=4)]
|
216
|
-
assert len(chunks) == 0
|
217
|
-
|
218
|
-
# Try with an iterable that yields chunks bigger than the chunk size
|
219
|
-
chunks = [
|
220
|
-
chunk
|
221
|
-
async for chunk in iter_in_chunk_size(
|
222
|
-
iterable(total_size=12, chunk_size=10), chunk_size=4
|
223
|
-
)
|
224
|
-
]
|
225
|
-
assert len(chunks) == 3
|
226
|
-
assert len(chunks[0]) == 4
|
227
|
-
assert len(chunks[1]) == 4
|
228
|
-
assert len(chunks[2]) == 4
|
@@ -1,72 +0,0 @@
|
|
1
|
-
# Copyright (C) 2021 Bosutech XXI S.L.
|
2
|
-
#
|
3
|
-
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
4
|
-
# For commercial licensing, contact us at info@nuclia.com.
|
5
|
-
#
|
6
|
-
# AGPL:
|
7
|
-
# This program is free software: you can redistribute it and/or modify
|
8
|
-
# it under the terms of the GNU Affero General Public License as
|
9
|
-
# published by the Free Software Foundation, either version 3 of the
|
10
|
-
# License, or (at your option) any later version.
|
11
|
-
#
|
12
|
-
# This program is distributed in the hope that it will be useful,
|
13
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
-
# GNU Affero General Public License for more details.
|
16
|
-
#
|
17
|
-
# You should have received a copy of the GNU Affero General Public License
|
18
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
-
import asyncio
|
20
|
-
import random
|
21
|
-
import time
|
22
|
-
|
23
|
-
import pytest
|
24
|
-
|
25
|
-
from nucliadb_utils.asyncio_utils import ConcurrentRunner, run_concurrently
|
26
|
-
|
27
|
-
|
28
|
-
async def test_run_concurrently():
|
29
|
-
async def mycoro(value):
|
30
|
-
await asyncio.sleep(random.uniform(0.0, 0.5))
|
31
|
-
return value
|
32
|
-
|
33
|
-
tasks = [mycoro(i) for i in range(10)]
|
34
|
-
results = await run_concurrently(tasks)
|
35
|
-
assert results == list(range(10))
|
36
|
-
|
37
|
-
|
38
|
-
async def test_concurrent_runner():
|
39
|
-
async def mycoro(value):
|
40
|
-
await asyncio.sleep(0.5)
|
41
|
-
return value
|
42
|
-
|
43
|
-
# Test without max_tasks: execution time should be less than 1 second
|
44
|
-
# as both should be executed in concurrently
|
45
|
-
runner = ConcurrentRunner()
|
46
|
-
runner.schedule(mycoro(1))
|
47
|
-
runner.schedule(mycoro(2))
|
48
|
-
start = time.perf_counter()
|
49
|
-
results = await runner.wait()
|
50
|
-
end = time.perf_counter()
|
51
|
-
assert results == [1, 2]
|
52
|
-
assert end - start < 1
|
53
|
-
|
54
|
-
# Test with max_tasks: execution time should be at least 1 second
|
55
|
-
runner = ConcurrentRunner(max_tasks=1)
|
56
|
-
runner.schedule(mycoro(1))
|
57
|
-
runner.schedule(mycoro(2))
|
58
|
-
start = time.perf_counter()
|
59
|
-
results = await runner.wait()
|
60
|
-
end = time.perf_counter()
|
61
|
-
assert results == [1, 2]
|
62
|
-
assert end - start >= 1
|
63
|
-
|
64
|
-
# Exception should be raised if any of the coroutines raises an exception
|
65
|
-
with pytest.raises(ValueError):
|
66
|
-
|
67
|
-
async def coro_with_exception():
|
68
|
-
raise ValueError("Test")
|
69
|
-
|
70
|
-
runner = ConcurrentRunner()
|
71
|
-
runner.schedule(coro_with_exception())
|
72
|
-
await runner.wait()
|
@@ -1,159 +0,0 @@
|
|
1
|
-
# Copyright (C) 2021 Bosutech XXI S.L.
|
2
|
-
#
|
3
|
-
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
4
|
-
# For commercial licensing, contact us at info@nuclia.com.
|
5
|
-
#
|
6
|
-
# AGPL:
|
7
|
-
# This program is free software: you can redistribute it and/or modify
|
8
|
-
# it under the terms of the GNU Affero General Public License as
|
9
|
-
# published by the Free Software Foundation, either version 3 of the
|
10
|
-
# License, or (at your option) any later version.
|
11
|
-
#
|
12
|
-
# This program is distributed in the hope that it will be useful,
|
13
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
-
# GNU Affero General Public License for more details.
|
16
|
-
#
|
17
|
-
# You should have received a copy of the GNU Affero General Public License
|
18
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
-
|
20
|
-
from unittest.mock import AsyncMock, Mock, patch
|
21
|
-
|
22
|
-
import pytest
|
23
|
-
from fastapi import WebSocket
|
24
|
-
from starlette.exceptions import HTTPException
|
25
|
-
from starlette.requests import Request
|
26
|
-
|
27
|
-
from nucliadb_utils import authentication
|
28
|
-
|
29
|
-
|
30
|
-
class TestNucliaCloudAuthenticationBackend:
|
31
|
-
@pytest.fixture()
|
32
|
-
def backend(self):
|
33
|
-
return authentication.NucliaCloudAuthenticationBackend()
|
34
|
-
|
35
|
-
@pytest.fixture()
|
36
|
-
def req(self):
|
37
|
-
return Mock(headers={})
|
38
|
-
|
39
|
-
@pytest.mark.asyncio
|
40
|
-
async def test_authenticate(
|
41
|
-
self, backend: authentication.NucliaCloudAuthenticationBackend, req
|
42
|
-
):
|
43
|
-
assert await backend.authenticate(req) is None
|
44
|
-
|
45
|
-
@pytest.mark.asyncio
|
46
|
-
async def test_authenticate_with_user(
|
47
|
-
self, backend: authentication.NucliaCloudAuthenticationBackend, req
|
48
|
-
):
|
49
|
-
req.headers = {
|
50
|
-
backend.roles_header: "admin",
|
51
|
-
backend.user_header: "user",
|
52
|
-
}
|
53
|
-
|
54
|
-
creds, user = await backend.authenticate(req)
|
55
|
-
|
56
|
-
assert creds.scopes == ["admin"]
|
57
|
-
assert user.username == "user"
|
58
|
-
|
59
|
-
@pytest.mark.asyncio
|
60
|
-
async def test_authenticate_with_anon(
|
61
|
-
self, backend: authentication.NucliaCloudAuthenticationBackend, req
|
62
|
-
):
|
63
|
-
req.headers = {backend.roles_header: "admin"}
|
64
|
-
|
65
|
-
creds, user = await backend.authenticate(req)
|
66
|
-
|
67
|
-
assert creds.scopes == ["admin"]
|
68
|
-
assert user.username == "anonymous"
|
69
|
-
|
70
|
-
|
71
|
-
def test_has_required_scope():
|
72
|
-
conn = Mock(auth=Mock(scopes=["admin"]))
|
73
|
-
assert authentication.has_required_scope(conn, ["admin"])
|
74
|
-
assert not authentication.has_required_scope(conn, ["foobar"])
|
75
|
-
|
76
|
-
|
77
|
-
class TestRequires:
|
78
|
-
def test_requires_sync(self):
|
79
|
-
req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
|
80
|
-
|
81
|
-
assert authentication.requires(["admin"])(lambda request: None)(req) is None
|
82
|
-
|
83
|
-
# test passed as kwargs
|
84
|
-
assert (
|
85
|
-
authentication.requires(["admin"])(lambda request: None)(request=req)
|
86
|
-
is None
|
87
|
-
)
|
88
|
-
|
89
|
-
def test_requires_sync_returns_status(self):
|
90
|
-
req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
|
91
|
-
|
92
|
-
with pytest.raises(HTTPException):
|
93
|
-
assert authentication.requires(["foobar"])(lambda request: None)(req)
|
94
|
-
|
95
|
-
def test_requires_sync_returns_redirect(self):
|
96
|
-
req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
|
97
|
-
|
98
|
-
with patch.object(req, "url_for", return_value="http://foobar"):
|
99
|
-
resp = authentication.requires(["foobar"], redirect="/foobar")(
|
100
|
-
lambda request: None
|
101
|
-
)(req)
|
102
|
-
assert resp.status_code == 303
|
103
|
-
|
104
|
-
@pytest.mark.asyncio
|
105
|
-
async def test_requires_async(self):
|
106
|
-
req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
|
107
|
-
|
108
|
-
async def noop(request): ...
|
109
|
-
|
110
|
-
assert await authentication.requires(["admin"])(noop)(request=req) is None
|
111
|
-
|
112
|
-
@pytest.mark.asyncio
|
113
|
-
async def test_requires_async_returns_status(self):
|
114
|
-
req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
|
115
|
-
|
116
|
-
async def noop(request): ...
|
117
|
-
|
118
|
-
with pytest.raises(HTTPException):
|
119
|
-
assert await authentication.requires(["foobar"])(noop)(request=req)
|
120
|
-
|
121
|
-
@pytest.mark.asyncio
|
122
|
-
async def test_requires_async_returns_redirect(self):
|
123
|
-
req = Request({"type": "http", "auth": Mock(scopes=["admin"])})
|
124
|
-
|
125
|
-
async def noop(request): ...
|
126
|
-
|
127
|
-
with patch.object(req, "url_for", return_value="http://foobar"):
|
128
|
-
resp = await authentication.requires(["foobar"], redirect="/foobar")(noop)(
|
129
|
-
request=req
|
130
|
-
)
|
131
|
-
assert resp.status_code == 303
|
132
|
-
|
133
|
-
@pytest.mark.asyncio
|
134
|
-
async def test_requires_ws(self):
|
135
|
-
ws = AsyncMock()
|
136
|
-
req = WebSocket(
|
137
|
-
{"type": "websocket", "auth": Mock(scopes=["admin"]), "websocket": ws},
|
138
|
-
receive=AsyncMock(),
|
139
|
-
send=AsyncMock(),
|
140
|
-
)
|
141
|
-
|
142
|
-
async def noop(websocket): ...
|
143
|
-
|
144
|
-
assert await authentication.requires(["admin"])(noop)(req) is None
|
145
|
-
|
146
|
-
@pytest.mark.asyncio
|
147
|
-
async def test_requires_ws_fail(self):
|
148
|
-
req = WebSocket(
|
149
|
-
{"type": "websocket", "auth": Mock(scopes=["admin"])},
|
150
|
-
receive=AsyncMock(),
|
151
|
-
send=AsyncMock(),
|
152
|
-
)
|
153
|
-
|
154
|
-
async def noop(websocket): ...
|
155
|
-
|
156
|
-
with patch.object(req, "close", return_value=None):
|
157
|
-
assert await authentication.requires(["notallowed"])(noop)(req) is None
|
158
|
-
|
159
|
-
req.close.assert_called_once()
|
@@ -1,45 +0,0 @@
|
|
1
|
-
# Copyright (C) 2021 Bosutech XXI S.L.
|
2
|
-
#
|
3
|
-
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
4
|
-
# For commercial licensing, contact us at info@nuclia.com.
|
5
|
-
#
|
6
|
-
# AGPL:
|
7
|
-
# This program is free software: you can redistribute it and/or modify
|
8
|
-
# it under the terms of the GNU Affero General Public License as
|
9
|
-
# published by the Free Software Foundation, either version 3 of the
|
10
|
-
# License, or (at your option) any later version.
|
11
|
-
#
|
12
|
-
# This program is distributed in the hope that it will be useful,
|
13
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
-
# GNU Affero General Public License for more details.
|
16
|
-
#
|
17
|
-
# You should have received a copy of the GNU Affero General Public License
|
18
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
-
#
|
20
|
-
|
21
|
-
from nucliadb_utils.helpers import async_gen_lookahead
|
22
|
-
|
23
|
-
|
24
|
-
async def test_async_gen_lookahead():
|
25
|
-
async def gen(n):
|
26
|
-
for i in range(n):
|
27
|
-
yield f"{i}".encode()
|
28
|
-
|
29
|
-
assert [item async for item in async_gen_lookahead(gen(0))] == []
|
30
|
-
assert [item async for item in async_gen_lookahead(gen(1))] == [(b"0", True)]
|
31
|
-
assert [item async for item in async_gen_lookahead(gen(2))] == [
|
32
|
-
(b"0", False),
|
33
|
-
(b"1", True),
|
34
|
-
]
|
35
|
-
|
36
|
-
|
37
|
-
async def test_async_gen_lookahead_last_chunk_is_empty():
|
38
|
-
async def gen():
|
39
|
-
for chunk in [b"empty", None, b"chunk", b""]:
|
40
|
-
yield chunk
|
41
|
-
|
42
|
-
assert [item async for item in async_gen_lookahead(gen())] == [
|
43
|
-
(b"empty", False),
|
44
|
-
(b"chunk", True),
|
45
|
-
]
|
@@ -1,154 +0,0 @@
|
|
1
|
-
# Copyright (C) 2021 Bosutech XXI S.L.
|
2
|
-
#
|
3
|
-
# nucliadb is offered under the AGPL v3.0 and as commercial software.
|
4
|
-
# For commercial licensing, contact us at info@nuclia.com.
|
5
|
-
#
|
6
|
-
# AGPL:
|
7
|
-
# This program is free software: you can redistribute it and/or modify
|
8
|
-
# it under the terms of the GNU Affero General Public License as
|
9
|
-
# published by the Free Software Foundation, either version 3 of the
|
10
|
-
# License, or (at your option) any later version.
|
11
|
-
#
|
12
|
-
# This program is distributed in the hope that it will be useful,
|
13
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
-
# GNU Affero General Public License for more details.
|
16
|
-
#
|
17
|
-
# You should have received a copy of the GNU Affero General Public License
|
18
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
-
|
20
|
-
import asyncio
|
21
|
-
import time
|
22
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
23
|
-
|
24
|
-
import pytest
|
25
|
-
|
26
|
-
from nucliadb_utils import nats
|
27
|
-
|
28
|
-
pytestmark = pytest.mark.unit
|
29
|
-
|
30
|
-
|
31
|
-
class TestNatsConnectionManager:
|
32
|
-
@pytest.fixture()
|
33
|
-
def nats_conn(self):
|
34
|
-
conn = MagicMock()
|
35
|
-
conn.drain = AsyncMock()
|
36
|
-
conn.close = AsyncMock()
|
37
|
-
with patch("nucliadb_utils.nats.nats.connect", return_value=conn):
|
38
|
-
yield conn
|
39
|
-
|
40
|
-
@pytest.fixture()
|
41
|
-
def js(self):
|
42
|
-
conn = AsyncMock()
|
43
|
-
with patch(
|
44
|
-
"nucliadb_utils.nats.get_traced_jetstream",
|
45
|
-
return_value=conn,
|
46
|
-
):
|
47
|
-
yield conn
|
48
|
-
|
49
|
-
@pytest.fixture()
|
50
|
-
def manager(self, nats_conn, js):
|
51
|
-
yield nats.NatsConnectionManager(service_name="test", nats_servers=["test"])
|
52
|
-
|
53
|
-
async def test_initialize(self, manager: nats.NatsConnectionManager, nats_conn):
|
54
|
-
await manager.initialize()
|
55
|
-
|
56
|
-
assert manager.nc == nats_conn
|
57
|
-
|
58
|
-
async def test_lifecycle_finalize(
|
59
|
-
self, manager: nats.NatsConnectionManager, nats_conn, js
|
60
|
-
):
|
61
|
-
await manager.initialize()
|
62
|
-
|
63
|
-
cb = AsyncMock()
|
64
|
-
lost_cb = AsyncMock()
|
65
|
-
await manager.subscribe(
|
66
|
-
subject="subject",
|
67
|
-
queue="queue",
|
68
|
-
stream="stream",
|
69
|
-
cb=cb,
|
70
|
-
subscription_lost_cb=lost_cb,
|
71
|
-
flow_control=True,
|
72
|
-
)
|
73
|
-
|
74
|
-
js.subscribe.assert_called_once_with(
|
75
|
-
subject="subject",
|
76
|
-
queue="queue",
|
77
|
-
stream="stream",
|
78
|
-
cb=cb,
|
79
|
-
manual_ack=True,
|
80
|
-
flow_control=True,
|
81
|
-
config=None,
|
82
|
-
)
|
83
|
-
|
84
|
-
await manager.reconnected_cb()
|
85
|
-
lost_cb.assert_called_once()
|
86
|
-
|
87
|
-
await manager.finalize()
|
88
|
-
|
89
|
-
nats_conn.drain.assert_called_once()
|
90
|
-
nats_conn.close.assert_called_once()
|
91
|
-
|
92
|
-
async def test_healthy(self, manager: nats.NatsConnectionManager):
|
93
|
-
await manager.initialize()
|
94
|
-
|
95
|
-
assert manager.healthy()
|
96
|
-
|
97
|
-
manager._healthy = False
|
98
|
-
assert not manager.healthy()
|
99
|
-
|
100
|
-
manager._healthy = True
|
101
|
-
manager._last_unhealthy = time.monotonic() - 100
|
102
|
-
assert not manager.healthy()
|
103
|
-
|
104
|
-
manager._last_unhealthy = None
|
105
|
-
manager._nc.is_connected = False
|
106
|
-
assert manager.healthy()
|
107
|
-
assert manager._last_unhealthy is not None
|
108
|
-
|
109
|
-
async def test_unsubscribe(
|
110
|
-
self, manager: nats.NatsConnectionManager, nats_conn, js
|
111
|
-
):
|
112
|
-
await manager.initialize()
|
113
|
-
|
114
|
-
cb = AsyncMock()
|
115
|
-
lost_cb = AsyncMock()
|
116
|
-
sub = await manager.subscribe(
|
117
|
-
subject="subject",
|
118
|
-
queue="queue",
|
119
|
-
stream="stream",
|
120
|
-
cb=cb,
|
121
|
-
subscription_lost_cb=lost_cb,
|
122
|
-
flow_control=True,
|
123
|
-
)
|
124
|
-
assert len(manager._subscriptions) == 1
|
125
|
-
|
126
|
-
await manager.unsubscribe(sub)
|
127
|
-
|
128
|
-
sub.unsubscribe.assert_awaited_once()
|
129
|
-
assert len(manager._subscriptions) == 0
|
130
|
-
|
131
|
-
await manager.finalize()
|
132
|
-
|
133
|
-
nats_conn.drain.assert_called_once()
|
134
|
-
nats_conn.close.assert_called_once()
|
135
|
-
|
136
|
-
|
137
|
-
async def test_message_progress_updater():
|
138
|
-
in_progress = AsyncMock()
|
139
|
-
msg = MagicMock(in_progress=in_progress, _ackd=False)
|
140
|
-
|
141
|
-
async with nats.MessageProgressUpdater(msg, 0.05):
|
142
|
-
await asyncio.sleep(0.07)
|
143
|
-
|
144
|
-
in_progress.assert_awaited_once()
|
145
|
-
|
146
|
-
|
147
|
-
async def test_message_progress_updater_does_not_update_ack():
|
148
|
-
in_progress = AsyncMock()
|
149
|
-
msg = MagicMock(in_progress=in_progress, _ackd=True)
|
150
|
-
|
151
|
-
async with nats.MessageProgressUpdater(msg, 0.05):
|
152
|
-
await asyncio.sleep(0.07)
|
153
|
-
|
154
|
-
in_progress.assert_not_awaited()
|