proximl 0.5.16__py3-none-any.whl → 1.0.0__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.
- examples/local_storage.py +0 -2
- proximl/__init__.py +1 -1
- proximl/checkpoints.py +56 -57
- proximl/cli/__init__.py +6 -3
- proximl/cli/checkpoint.py +18 -57
- proximl/cli/dataset.py +17 -57
- proximl/cli/job/__init__.py +11 -53
- proximl/cli/job/create.py +51 -24
- proximl/cli/model.py +14 -56
- proximl/cli/volume.py +18 -57
- proximl/datasets.py +50 -55
- proximl/jobs.py +239 -68
- proximl/models.py +51 -55
- proximl/projects/projects.py +2 -2
- proximl/proximl.py +50 -16
- proximl/utils/__init__.py +1 -0
- proximl/{auth.py → utils/auth.py} +4 -3
- proximl/utils/transfer.py +587 -0
- proximl/volumes.py +48 -53
- {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/METADATA +3 -3
- {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/RECORD +53 -51
- tests/integration/test_checkpoints_integration.py +4 -3
- tests/integration/test_datasets_integration.py +5 -3
- tests/integration/test_jobs_integration.py +33 -27
- tests/integration/test_models_integration.py +7 -3
- tests/integration/test_volumes_integration.py +2 -2
- tests/unit/cli/test_cli_checkpoint_unit.py +312 -1
- tests/unit/cloudbender/test_nodes_unit.py +112 -0
- tests/unit/cloudbender/test_providers_unit.py +96 -0
- tests/unit/cloudbender/test_regions_unit.py +106 -0
- tests/unit/cloudbender/test_services_unit.py +141 -0
- tests/unit/conftest.py +23 -10
- tests/unit/projects/test_project_data_connectors_unit.py +39 -0
- tests/unit/projects/test_project_datastores_unit.py +37 -0
- tests/unit/projects/test_project_members_unit.py +46 -0
- tests/unit/projects/test_project_services_unit.py +65 -0
- tests/unit/projects/test_projects_unit.py +17 -1
- tests/unit/test_auth_unit.py +17 -2
- tests/unit/test_checkpoints_unit.py +256 -71
- tests/unit/test_datasets_unit.py +218 -68
- tests/unit/test_exceptions.py +133 -0
- tests/unit/test_gpu_types_unit.py +11 -1
- tests/unit/test_jobs_unit.py +1014 -95
- tests/unit/test_main_unit.py +20 -0
- tests/unit/test_models_unit.py +218 -70
- tests/unit/test_proximl_unit.py +627 -3
- tests/unit/test_volumes_unit.py +211 -70
- tests/unit/utils/__init__.py +1 -0
- tests/unit/utils/test_transfer_unit.py +4260 -0
- proximl/cli/connection.py +0 -61
- proximl/connections.py +0 -621
- tests/unit/test_connections_unit.py +0 -182
- {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/LICENSE +0 -0
- {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/WHEEL +0 -0
- {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/entry_points.txt +0 -0
- {proximl-0.5.16.dist-info → proximl-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import json
|
|
3
3
|
import click
|
|
4
|
-
from unittest.mock import AsyncMock, patch
|
|
4
|
+
from unittest.mock import AsyncMock, patch, Mock
|
|
5
5
|
from pytest import mark, fixture, raises
|
|
6
6
|
|
|
7
7
|
pytestmark = [mark.cli, mark.unit, mark.checkpoints]
|
|
8
8
|
|
|
9
9
|
from proximl.cli import checkpoint as specimen
|
|
10
|
+
from proximl.cli.checkpoint import pretty_size
|
|
10
11
|
from proximl.checkpoints import Checkpoint
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def test_pretty_size_zero():
|
|
15
|
+
"""Test pretty_size with zero/None (line 7)."""
|
|
16
|
+
result = pretty_size(None)
|
|
17
|
+
assert result == "0.00 B"
|
|
18
|
+
result = pretty_size(0)
|
|
19
|
+
assert result == "0.00 B"
|
|
20
|
+
|
|
21
|
+
|
|
13
22
|
def test_list(runner, mock_my_checkpoints):
|
|
14
23
|
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
15
24
|
mock_proximl.checkpoints = AsyncMock()
|
|
@@ -20,3 +29,305 @@ def test_list(runner, mock_my_checkpoints):
|
|
|
20
29
|
print(result)
|
|
21
30
|
assert result.exit_code == 0
|
|
22
31
|
mock_proximl.checkpoints.list.assert_called_once()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_attach_success(runner, mock_my_checkpoints):
|
|
35
|
+
"""Test attach command success (lines 32-38)."""
|
|
36
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
37
|
+
|
|
38
|
+
async def list_async():
|
|
39
|
+
return mock_my_checkpoints
|
|
40
|
+
|
|
41
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
42
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
43
|
+
|
|
44
|
+
# Use the first checkpoint from the list
|
|
45
|
+
checkpoint = mock_my_checkpoints[0]
|
|
46
|
+
|
|
47
|
+
async def attach_async():
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
checkpoint.attach = Mock(return_value=attach_async())
|
|
51
|
+
|
|
52
|
+
with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
|
|
53
|
+
result = runner.invoke(specimen, ["attach", "1"])
|
|
54
|
+
assert result.exit_code == 0
|
|
55
|
+
checkpoint.attach.assert_called_once()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_attach_not_found(runner, mock_my_checkpoints):
|
|
59
|
+
"""Test attach command when checkpoint not found (line 36)."""
|
|
60
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
61
|
+
|
|
62
|
+
async def list_async():
|
|
63
|
+
return mock_my_checkpoints
|
|
64
|
+
|
|
65
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
66
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
67
|
+
|
|
68
|
+
with patch("proximl.cli.search_by_id_name", return_value=None):
|
|
69
|
+
result = runner.invoke(specimen, ["attach", "nonexistent"])
|
|
70
|
+
assert result.exit_code != 0
|
|
71
|
+
assert "Cannot find specified checkpoint" in result.output
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_connect_with_attach(runner, mock_my_checkpoints):
|
|
75
|
+
"""Test connect command with attach (lines 56-65, attach=True)."""
|
|
76
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
77
|
+
|
|
78
|
+
async def list_async():
|
|
79
|
+
return mock_my_checkpoints
|
|
80
|
+
|
|
81
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
82
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
83
|
+
|
|
84
|
+
checkpoint = mock_my_checkpoints[0]
|
|
85
|
+
|
|
86
|
+
async def connect_async():
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
async def attach_async():
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
checkpoint.connect = Mock(return_value=connect_async())
|
|
93
|
+
checkpoint.attach = Mock(return_value=attach_async())
|
|
94
|
+
|
|
95
|
+
with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
|
|
96
|
+
result = runner.invoke(specimen, ["connect", "1"])
|
|
97
|
+
assert result.exit_code == 0
|
|
98
|
+
checkpoint.connect.assert_called_once()
|
|
99
|
+
checkpoint.attach.assert_called_once()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_connect_no_attach(runner, mock_my_checkpoints):
|
|
103
|
+
"""Test connect command without attach (lines 56-65, attach=False)."""
|
|
104
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
105
|
+
|
|
106
|
+
async def list_async():
|
|
107
|
+
return mock_my_checkpoints
|
|
108
|
+
|
|
109
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
110
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
111
|
+
|
|
112
|
+
checkpoint = mock_my_checkpoints[0]
|
|
113
|
+
|
|
114
|
+
async def connect_async():
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
checkpoint.connect = Mock(return_value=connect_async())
|
|
118
|
+
|
|
119
|
+
with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
|
|
120
|
+
result = runner.invoke(specimen, ["connect", "--no-attach", "1"])
|
|
121
|
+
assert result.exit_code == 0
|
|
122
|
+
checkpoint.connect.assert_called_once()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_connect_not_found(runner, mock_my_checkpoints):
|
|
126
|
+
"""Test connect command when checkpoint not found (line 60)."""
|
|
127
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
128
|
+
|
|
129
|
+
async def list_async():
|
|
130
|
+
return mock_my_checkpoints
|
|
131
|
+
|
|
132
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
133
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
134
|
+
|
|
135
|
+
with patch("proximl.cli.search_by_id_name", return_value=None):
|
|
136
|
+
result = runner.invoke(specimen, ["connect", "nonexistent"])
|
|
137
|
+
assert result.exit_code != 0
|
|
138
|
+
assert "Cannot find specified checkpoint" in result.output
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_create_with_connect_and_attach(runner, tmp_path, mock_my_checkpoints):
|
|
142
|
+
"""Test create command with connect and attach (lines 103-115)."""
|
|
143
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
144
|
+
checkpoint = mock_my_checkpoints[0]
|
|
145
|
+
|
|
146
|
+
async def connect_async():
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
async def attach_async():
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
checkpoint.connect = Mock(return_value=connect_async())
|
|
153
|
+
checkpoint.attach = Mock(return_value=attach_async())
|
|
154
|
+
|
|
155
|
+
async def create_async(**kwargs):
|
|
156
|
+
return checkpoint
|
|
157
|
+
|
|
158
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
159
|
+
mock_proximl.checkpoints.create = Mock(
|
|
160
|
+
side_effect=lambda **kwargs: create_async(**kwargs)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
test_dir = tmp_path / "test_checkpoint"
|
|
164
|
+
test_dir.mkdir()
|
|
165
|
+
result = runner.invoke(
|
|
166
|
+
specimen, ["create", "test-checkpoint", str(test_dir)]
|
|
167
|
+
)
|
|
168
|
+
assert result.exit_code == 0
|
|
169
|
+
mock_proximl.checkpoints.create.assert_called_once()
|
|
170
|
+
checkpoint.connect.assert_called_once()
|
|
171
|
+
checkpoint.attach.assert_called_once()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_create_with_connect_no_attach(runner, tmp_path, mock_my_checkpoints):
|
|
175
|
+
"""Test create command with connect but no attach (lines 103-115)."""
|
|
176
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
177
|
+
checkpoint = mock_my_checkpoints[0]
|
|
178
|
+
|
|
179
|
+
async def connect_async():
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
checkpoint.connect = Mock(return_value=connect_async())
|
|
183
|
+
|
|
184
|
+
async def create_async(**kwargs):
|
|
185
|
+
return checkpoint
|
|
186
|
+
|
|
187
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
188
|
+
mock_proximl.checkpoints.create = Mock(
|
|
189
|
+
side_effect=lambda **kwargs: create_async(**kwargs)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
test_dir = tmp_path / "test_checkpoint"
|
|
193
|
+
test_dir.mkdir()
|
|
194
|
+
result = runner.invoke(
|
|
195
|
+
specimen,
|
|
196
|
+
["create", "--no-attach", "test-checkpoint", str(test_dir)],
|
|
197
|
+
)
|
|
198
|
+
assert result.exit_code == 0
|
|
199
|
+
checkpoint.connect.assert_called_once()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_create_no_connect(runner, tmp_path):
|
|
203
|
+
"""Test create command without connect (lines 103-115, line 115)."""
|
|
204
|
+
mock_checkpoint = Mock(spec=Checkpoint)
|
|
205
|
+
|
|
206
|
+
mock_proximl_runner = Mock()
|
|
207
|
+
mock_proximl_runner.client = Mock()
|
|
208
|
+
mock_proximl_runner.client.checkpoints = Mock()
|
|
209
|
+
mock_proximl_runner.client.checkpoints.create = AsyncMock(
|
|
210
|
+
return_value=mock_checkpoint
|
|
211
|
+
)
|
|
212
|
+
mock_proximl_runner.run = Mock(
|
|
213
|
+
side_effect=lambda x: x if not hasattr(x, "__call__") else x()
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
with patch("proximl.cli.ProxiMLRunner", return_value=mock_proximl_runner):
|
|
217
|
+
test_dir = tmp_path / "test_checkpoint"
|
|
218
|
+
test_dir.mkdir()
|
|
219
|
+
result = runner.invoke(
|
|
220
|
+
specimen,
|
|
221
|
+
["create", "--no-connect", "test-checkpoint", str(test_dir)],
|
|
222
|
+
)
|
|
223
|
+
assert result.exit_code != 0
|
|
224
|
+
assert "No logs to show" in result.output
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_list_public(runner, mock_my_checkpoints):
|
|
228
|
+
"""Test list_public command (lines 152-171)."""
|
|
229
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
230
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
231
|
+
mock_proximl.checkpoints.list_public = AsyncMock(
|
|
232
|
+
return_value=mock_my_checkpoints
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result = runner.invoke(specimen, ["list-public"])
|
|
236
|
+
assert result.exit_code == 0
|
|
237
|
+
mock_proximl.checkpoints.list_public.assert_called_once()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_remove_success(runner, mock_my_checkpoints):
|
|
241
|
+
"""Test remove command success (lines 192-201)."""
|
|
242
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
243
|
+
|
|
244
|
+
async def list_async():
|
|
245
|
+
return mock_my_checkpoints
|
|
246
|
+
|
|
247
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
248
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
249
|
+
|
|
250
|
+
checkpoint = mock_my_checkpoints[0]
|
|
251
|
+
|
|
252
|
+
async def remove_async():
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
checkpoint.remove = Mock(return_value=remove_async())
|
|
256
|
+
|
|
257
|
+
with patch("proximl.cli.search_by_id_name", return_value=checkpoint):
|
|
258
|
+
result = runner.invoke(specimen, ["remove", "1"])
|
|
259
|
+
assert result.exit_code == 0
|
|
260
|
+
checkpoint.remove.assert_called_once_with(force=False)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_remove_not_found(runner, mock_my_checkpoints):
|
|
264
|
+
"""Test remove command when checkpoint not found (lines 192-201)."""
|
|
265
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
266
|
+
|
|
267
|
+
async def list_async():
|
|
268
|
+
return mock_my_checkpoints
|
|
269
|
+
|
|
270
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
271
|
+
mock_proximl.checkpoints.list = Mock(side_effect=lambda: list_async())
|
|
272
|
+
|
|
273
|
+
with patch("proximl.cli.search_by_id_name", return_value=None):
|
|
274
|
+
result = runner.invoke(specimen, ["remove", "nonexistent"])
|
|
275
|
+
assert result.exit_code != 0
|
|
276
|
+
assert "Cannot find specified checkpoint" in result.output
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_rename_success(runner, mock_my_checkpoints):
|
|
280
|
+
"""Test rename command success (lines 214-223)."""
|
|
281
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
282
|
+
checkpoint = mock_my_checkpoints[0]
|
|
283
|
+
|
|
284
|
+
async def rename_async():
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
checkpoint.rename = Mock(return_value=rename_async())
|
|
288
|
+
|
|
289
|
+
async def get_async(checkpoint_id):
|
|
290
|
+
return checkpoint
|
|
291
|
+
|
|
292
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
293
|
+
mock_proximl.checkpoints.get = Mock(
|
|
294
|
+
side_effect=lambda checkpoint_id: get_async(checkpoint_id)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
result = runner.invoke(specimen, ["rename", "1", "new-name"])
|
|
298
|
+
assert result.exit_code == 0
|
|
299
|
+
checkpoint.rename.assert_called_once_with(name="new-name")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_rename_not_found_none(runner):
|
|
303
|
+
"""Test rename command when checkpoint is None (lines 214-223, line 219)."""
|
|
304
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
305
|
+
|
|
306
|
+
async def get_async(checkpoint_id):
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
310
|
+
mock_proximl.checkpoints.get = Mock(
|
|
311
|
+
side_effect=lambda checkpoint_id: get_async(checkpoint_id)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
result = runner.invoke(specimen, ["rename", "nonexistent", "new-name"])
|
|
315
|
+
assert result.exit_code != 0
|
|
316
|
+
assert "Cannot find specified checkpoint" in result.output
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_rename_not_found_exception(runner):
|
|
320
|
+
"""Test rename command when exception occurs (lines 214-223, line 221)."""
|
|
321
|
+
with patch("proximl.cli.ProxiML", new=AsyncMock) as mock_proximl:
|
|
322
|
+
|
|
323
|
+
async def get_async(checkpoint_id):
|
|
324
|
+
raise Exception("Not found")
|
|
325
|
+
|
|
326
|
+
mock_proximl.checkpoints = AsyncMock()
|
|
327
|
+
mock_proximl.checkpoints.get = Mock(
|
|
328
|
+
side_effect=lambda checkpoint_id: get_async(checkpoint_id)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
result = runner.invoke(specimen, ["rename", "nonexistent", "new-name"])
|
|
332
|
+
assert result.exit_code != 0
|
|
333
|
+
assert "Cannot find specified checkpoint" in result.output
|
|
@@ -8,6 +8,7 @@ from aiohttp import WSMessage, WSMsgType
|
|
|
8
8
|
import proximl.cloudbender.nodes as specimen
|
|
9
9
|
from proximl.exceptions import (
|
|
10
10
|
ApiError,
|
|
11
|
+
NodeError,
|
|
11
12
|
SpecificationError,
|
|
12
13
|
ProxiMLException,
|
|
13
14
|
)
|
|
@@ -200,3 +201,114 @@ class nodeTests:
|
|
|
200
201
|
None,
|
|
201
202
|
dict(command="report"),
|
|
202
203
|
)
|
|
204
|
+
|
|
205
|
+
@mark.asyncio
|
|
206
|
+
async def test_node_wait_for_already_at_status(self, node):
|
|
207
|
+
"""Test wait_for returns immediately if already at target status."""
|
|
208
|
+
node._status = "active"
|
|
209
|
+
result = await node.wait_for("active")
|
|
210
|
+
assert result is None
|
|
211
|
+
|
|
212
|
+
@mark.asyncio
|
|
213
|
+
async def test_node_wait_for_invalid_status(self, node):
|
|
214
|
+
"""Test wait_for raises error for invalid status."""
|
|
215
|
+
with raises(SpecificationError) as exc_info:
|
|
216
|
+
await node.wait_for("invalid_status")
|
|
217
|
+
assert "Invalid wait_for status" in str(exc_info.value.message)
|
|
218
|
+
|
|
219
|
+
@mark.asyncio
|
|
220
|
+
async def test_node_wait_for_timeout_validation(self, node):
|
|
221
|
+
"""Test wait_for validates timeout (line 172)."""
|
|
222
|
+
node._status = "new" # Set to different status so timeout check runs
|
|
223
|
+
with raises(SpecificationError) as exc_info:
|
|
224
|
+
await node.wait_for("active", timeout=25 * 60 * 60)
|
|
225
|
+
assert "timeout must be less than" in str(exc_info.value.message)
|
|
226
|
+
|
|
227
|
+
@mark.asyncio
|
|
228
|
+
async def test_node_wait_for_success(self, node, mock_proximl):
|
|
229
|
+
"""Test wait_for succeeds when status matches."""
|
|
230
|
+
node._status = "new"
|
|
231
|
+
api_response_new = dict(
|
|
232
|
+
provider_uuid="1",
|
|
233
|
+
region_uuid="a",
|
|
234
|
+
rig_uuid="x",
|
|
235
|
+
status="new",
|
|
236
|
+
)
|
|
237
|
+
api_response_active = dict(
|
|
238
|
+
provider_uuid="1",
|
|
239
|
+
region_uuid="a",
|
|
240
|
+
rig_uuid="x",
|
|
241
|
+
status="active",
|
|
242
|
+
)
|
|
243
|
+
mock_proximl._query = AsyncMock(
|
|
244
|
+
side_effect=[api_response_new, api_response_active]
|
|
245
|
+
)
|
|
246
|
+
with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
|
|
247
|
+
result = await node.wait_for("active", timeout=10)
|
|
248
|
+
assert result == node
|
|
249
|
+
assert node.status == "active"
|
|
250
|
+
|
|
251
|
+
@mark.asyncio
|
|
252
|
+
async def test_node_wait_for_archived_404(self, node, mock_proximl):
|
|
253
|
+
"""Test wait_for handles 404 for archived status."""
|
|
254
|
+
node._status = "active"
|
|
255
|
+
api_error = ApiError(404, {"errorMessage": "Not found"})
|
|
256
|
+
mock_proximl._query = AsyncMock(side_effect=api_error)
|
|
257
|
+
with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
|
|
258
|
+
await node.wait_for("archived", timeout=10)
|
|
259
|
+
|
|
260
|
+
@mark.asyncio
|
|
261
|
+
async def test_node_wait_for_error_status(self, node, mock_proximl):
|
|
262
|
+
"""Test wait_for raises error for errored/failed status."""
|
|
263
|
+
node._status = "new"
|
|
264
|
+
api_response_errored = dict(
|
|
265
|
+
provider_uuid="1",
|
|
266
|
+
region_uuid="a",
|
|
267
|
+
rig_uuid="x",
|
|
268
|
+
status="errored",
|
|
269
|
+
)
|
|
270
|
+
mock_proximl._query = AsyncMock(return_value=api_response_errored)
|
|
271
|
+
with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
|
|
272
|
+
with raises(NodeError):
|
|
273
|
+
await node.wait_for("active", timeout=10)
|
|
274
|
+
|
|
275
|
+
@mark.asyncio
|
|
276
|
+
async def test_node_wait_for_timeout(self, node, mock_proximl):
|
|
277
|
+
"""Test wait_for raises timeout exception."""
|
|
278
|
+
node._status = "new"
|
|
279
|
+
api_response_new = dict(
|
|
280
|
+
provider_uuid="1",
|
|
281
|
+
region_uuid="a",
|
|
282
|
+
rig_uuid="x",
|
|
283
|
+
status="new",
|
|
284
|
+
)
|
|
285
|
+
mock_proximl._query = AsyncMock(return_value=api_response_new)
|
|
286
|
+
with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
|
|
287
|
+
with raises(ProxiMLException) as exc_info:
|
|
288
|
+
await node.wait_for("active", timeout=0.1)
|
|
289
|
+
assert "Timeout waiting for" in str(exc_info.value.message)
|
|
290
|
+
|
|
291
|
+
@mark.asyncio
|
|
292
|
+
async def test_node_wait_for_api_error_non_404(self, node, mock_proximl):
|
|
293
|
+
"""Test wait_for raises ApiError when not 404 for archived (line 189)."""
|
|
294
|
+
node._status = "active"
|
|
295
|
+
api_error = ApiError(500, {"errorMessage": "Server Error"})
|
|
296
|
+
mock_proximl._query = AsyncMock(side_effect=api_error)
|
|
297
|
+
with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
|
|
298
|
+
with raises(ApiError):
|
|
299
|
+
await node.wait_for("archived", timeout=10)
|
|
300
|
+
|
|
301
|
+
@mark.asyncio
|
|
302
|
+
async def test_node_wait_for_failed_status(self, node, mock_proximl):
|
|
303
|
+
"""Test wait_for raises error for failed status (line 191)."""
|
|
304
|
+
node._status = "new"
|
|
305
|
+
api_response_failed = dict(
|
|
306
|
+
provider_uuid="1",
|
|
307
|
+
region_uuid="a",
|
|
308
|
+
rig_uuid="x",
|
|
309
|
+
status="failed",
|
|
310
|
+
)
|
|
311
|
+
mock_proximl._query = AsyncMock(return_value=api_response_failed)
|
|
312
|
+
with patch("proximl.cloudbender.nodes.asyncio.sleep", new_callable=AsyncMock):
|
|
313
|
+
with raises(NodeError):
|
|
314
|
+
await node.wait_for("active", timeout=10)
|
|
@@ -139,3 +139,99 @@ class providerTests:
|
|
|
139
139
|
mock_proximl._query.assert_called_once_with(f"/provider/1", "GET")
|
|
140
140
|
assert provider.id == "provider-id-1"
|
|
141
141
|
assert response.id == "provider-id-1"
|
|
142
|
+
|
|
143
|
+
def test_provider_status_property(self, provider):
|
|
144
|
+
"""Test provider status property."""
|
|
145
|
+
provider._status = "ready"
|
|
146
|
+
assert provider.status == "ready"
|
|
147
|
+
|
|
148
|
+
@mark.asyncio
|
|
149
|
+
async def test_provider_wait_for_already_at_status(self, provider):
|
|
150
|
+
"""Test wait_for returns immediately if already at target status."""
|
|
151
|
+
provider._status = "ready"
|
|
152
|
+
result = await provider.wait_for("ready")
|
|
153
|
+
assert result is None
|
|
154
|
+
|
|
155
|
+
@mark.asyncio
|
|
156
|
+
async def test_provider_wait_for_invalid_status(self, provider):
|
|
157
|
+
"""Test wait_for raises error for invalid status."""
|
|
158
|
+
with raises(SpecificationError) as exc_info:
|
|
159
|
+
await provider.wait_for("invalid_status")
|
|
160
|
+
assert "Invalid wait_for status" in str(exc_info.value.message)
|
|
161
|
+
|
|
162
|
+
@mark.asyncio
|
|
163
|
+
async def test_provider_wait_for_timeout_validation(self, provider):
|
|
164
|
+
"""Test wait_for validates timeout."""
|
|
165
|
+
with raises(SpecificationError) as exc_info:
|
|
166
|
+
await provider.wait_for("ready", timeout=25 * 60 * 60)
|
|
167
|
+
assert "timeout must be less than" in str(exc_info.value.message)
|
|
168
|
+
|
|
169
|
+
@mark.asyncio
|
|
170
|
+
async def test_provider_wait_for_success(self, provider, mock_proximl):
|
|
171
|
+
"""Test wait_for succeeds when status matches."""
|
|
172
|
+
provider._status = "new"
|
|
173
|
+
api_response_new = dict(
|
|
174
|
+
customer_uuid="a",
|
|
175
|
+
provider_uuid="1",
|
|
176
|
+
status="new",
|
|
177
|
+
)
|
|
178
|
+
api_response_ready = dict(
|
|
179
|
+
customer_uuid="a",
|
|
180
|
+
provider_uuid="1",
|
|
181
|
+
status="ready",
|
|
182
|
+
)
|
|
183
|
+
mock_proximl._query = AsyncMock(
|
|
184
|
+
side_effect=[api_response_new, api_response_ready]
|
|
185
|
+
)
|
|
186
|
+
with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
|
|
187
|
+
result = await provider.wait_for("ready", timeout=10)
|
|
188
|
+
assert result == provider
|
|
189
|
+
assert provider.status == "ready"
|
|
190
|
+
|
|
191
|
+
@mark.asyncio
|
|
192
|
+
async def test_provider_wait_for_archived_404(self, provider, mock_proximl):
|
|
193
|
+
"""Test wait_for handles 404 for archived status."""
|
|
194
|
+
provider._status = "ready"
|
|
195
|
+
api_error = ApiError(404, {"errorMessage": "Not found"})
|
|
196
|
+
mock_proximl._query = AsyncMock(side_effect=api_error)
|
|
197
|
+
with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
|
|
198
|
+
await provider.wait_for("archived", timeout=10)
|
|
199
|
+
|
|
200
|
+
@mark.asyncio
|
|
201
|
+
async def test_provider_wait_for_error_status(self, provider, mock_proximl):
|
|
202
|
+
"""Test wait_for raises error for errored/failed status."""
|
|
203
|
+
provider._status = "new"
|
|
204
|
+
api_response_errored = dict(
|
|
205
|
+
customer_uuid="a",
|
|
206
|
+
provider_uuid="1",
|
|
207
|
+
status="errored",
|
|
208
|
+
)
|
|
209
|
+
mock_proximl._query = AsyncMock(return_value=api_response_errored)
|
|
210
|
+
with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
|
|
211
|
+
with raises(specimen.ProviderError):
|
|
212
|
+
await provider.wait_for("ready", timeout=10)
|
|
213
|
+
|
|
214
|
+
@mark.asyncio
|
|
215
|
+
async def test_provider_wait_for_timeout(self, provider, mock_proximl):
|
|
216
|
+
"""Test wait_for raises timeout exception."""
|
|
217
|
+
provider._status = "new"
|
|
218
|
+
api_response_new = dict(
|
|
219
|
+
customer_uuid="a",
|
|
220
|
+
provider_uuid="1",
|
|
221
|
+
status="new",
|
|
222
|
+
)
|
|
223
|
+
mock_proximl._query = AsyncMock(return_value=api_response_new)
|
|
224
|
+
with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
|
|
225
|
+
with raises(ProxiMLException) as exc_info:
|
|
226
|
+
await provider.wait_for("ready", timeout=0.1)
|
|
227
|
+
assert "Timeout waiting for" in str(exc_info.value.message)
|
|
228
|
+
|
|
229
|
+
@mark.asyncio
|
|
230
|
+
async def test_provider_wait_for_api_error_non_404(self, provider, mock_proximl):
|
|
231
|
+
"""Test wait_for raises ApiError when not 404 for archived (line 115)."""
|
|
232
|
+
provider._status = "ready"
|
|
233
|
+
api_error = ApiError(500, {"errorMessage": "Server Error"})
|
|
234
|
+
mock_proximl._query = AsyncMock(side_effect=api_error)
|
|
235
|
+
with patch("proximl.cloudbender.providers.asyncio.sleep", new_callable=AsyncMock):
|
|
236
|
+
with raises(ApiError):
|
|
237
|
+
await provider.wait_for("archived", timeout=10)
|
|
@@ -195,3 +195,109 @@ class regionTests:
|
|
|
195
195
|
mock_proximl._query.assert_called_once_with(
|
|
196
196
|
"/provider/1/region/a/checkpoint", "POST", None, expected_payload
|
|
197
197
|
)
|
|
198
|
+
|
|
199
|
+
@mark.asyncio
|
|
200
|
+
async def test_region_wait_for_already_at_status(self, region):
|
|
201
|
+
"""Test wait_for returns immediately if already at target status."""
|
|
202
|
+
region._status = "healthy"
|
|
203
|
+
result = await region.wait_for("healthy")
|
|
204
|
+
assert result is None
|
|
205
|
+
|
|
206
|
+
@mark.asyncio
|
|
207
|
+
async def test_region_wait_for_invalid_status(self, region):
|
|
208
|
+
"""Test wait_for raises error for invalid status."""
|
|
209
|
+
with raises(SpecificationError) as exc_info:
|
|
210
|
+
await region.wait_for("invalid_status")
|
|
211
|
+
assert "Invalid wait_for status" in str(exc_info.value.message)
|
|
212
|
+
|
|
213
|
+
@mark.asyncio
|
|
214
|
+
async def test_region_wait_for_timeout_validation(self, region):
|
|
215
|
+
"""Test wait_for validates timeout (line 135)."""
|
|
216
|
+
region._status = "new" # Set to different status so timeout check runs
|
|
217
|
+
with raises(SpecificationError) as exc_info:
|
|
218
|
+
await region.wait_for("healthy", timeout=25 * 60 * 60)
|
|
219
|
+
assert "timeout must be less than" in str(exc_info.value.message)
|
|
220
|
+
|
|
221
|
+
@mark.asyncio
|
|
222
|
+
async def test_region_wait_for_success(self, region, mock_proximl):
|
|
223
|
+
"""Test wait_for succeeds when status matches."""
|
|
224
|
+
region._status = "new"
|
|
225
|
+
api_response_new = dict(
|
|
226
|
+
provider_uuid="1",
|
|
227
|
+
region_uuid="a",
|
|
228
|
+
status="new",
|
|
229
|
+
)
|
|
230
|
+
api_response_healthy = dict(
|
|
231
|
+
provider_uuid="1",
|
|
232
|
+
region_uuid="a",
|
|
233
|
+
status="healthy",
|
|
234
|
+
)
|
|
235
|
+
mock_proximl._query = AsyncMock(
|
|
236
|
+
side_effect=[api_response_new, api_response_healthy]
|
|
237
|
+
)
|
|
238
|
+
with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
|
|
239
|
+
result = await region.wait_for("healthy", timeout=10)
|
|
240
|
+
assert result == region
|
|
241
|
+
assert region.status == "healthy"
|
|
242
|
+
|
|
243
|
+
@mark.asyncio
|
|
244
|
+
async def test_region_wait_for_archived_404(self, region, mock_proximl):
|
|
245
|
+
"""Test wait_for handles 404 for archived status."""
|
|
246
|
+
region._status = "healthy"
|
|
247
|
+
api_error = ApiError(404, {"errorMessage": "Not found"})
|
|
248
|
+
mock_proximl._query = AsyncMock(side_effect=api_error)
|
|
249
|
+
with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
|
|
250
|
+
await region.wait_for("archived", timeout=10)
|
|
251
|
+
|
|
252
|
+
@mark.asyncio
|
|
253
|
+
async def test_region_wait_for_error_status(self, region, mock_proximl):
|
|
254
|
+
"""Test wait_for raises error for errored/failed status."""
|
|
255
|
+
region._status = "new"
|
|
256
|
+
api_response_errored = dict(
|
|
257
|
+
provider_uuid="1",
|
|
258
|
+
region_uuid="a",
|
|
259
|
+
status="errored",
|
|
260
|
+
)
|
|
261
|
+
mock_proximl._query = AsyncMock(return_value=api_response_errored)
|
|
262
|
+
with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
|
|
263
|
+
with raises(specimen.RegionError):
|
|
264
|
+
await region.wait_for("healthy", timeout=10)
|
|
265
|
+
|
|
266
|
+
@mark.asyncio
|
|
267
|
+
async def test_region_wait_for_timeout(self, region, mock_proximl):
|
|
268
|
+
"""Test wait_for raises timeout exception."""
|
|
269
|
+
region._status = "new"
|
|
270
|
+
api_response_new = dict(
|
|
271
|
+
provider_uuid="1",
|
|
272
|
+
region_uuid="a",
|
|
273
|
+
status="new",
|
|
274
|
+
)
|
|
275
|
+
mock_proximl._query = AsyncMock(return_value=api_response_new)
|
|
276
|
+
with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
|
|
277
|
+
with raises(ProxiMLException) as exc_info:
|
|
278
|
+
await region.wait_for("healthy", timeout=0.1)
|
|
279
|
+
assert "Timeout waiting for" in str(exc_info.value.message)
|
|
280
|
+
|
|
281
|
+
@mark.asyncio
|
|
282
|
+
async def test_region_wait_for_api_error_non_404(self, region, mock_proximl):
|
|
283
|
+
"""Test wait_for raises ApiError when not 404 for archived (line 152)."""
|
|
284
|
+
region._status = "healthy"
|
|
285
|
+
api_error = ApiError(500, {"errorMessage": "Server Error"})
|
|
286
|
+
mock_proximl._query = AsyncMock(side_effect=api_error)
|
|
287
|
+
with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
|
|
288
|
+
with raises(ApiError):
|
|
289
|
+
await region.wait_for("archived", timeout=10)
|
|
290
|
+
|
|
291
|
+
@mark.asyncio
|
|
292
|
+
async def test_region_wait_for_failed_status(self, region, mock_proximl):
|
|
293
|
+
"""Test wait_for raises error for failed status."""
|
|
294
|
+
region._status = "new"
|
|
295
|
+
api_response_failed = dict(
|
|
296
|
+
provider_uuid="1",
|
|
297
|
+
region_uuid="a",
|
|
298
|
+
status="failed",
|
|
299
|
+
)
|
|
300
|
+
mock_proximl._query = AsyncMock(return_value=api_response_failed)
|
|
301
|
+
with patch("proximl.cloudbender.regions.asyncio.sleep", new_callable=AsyncMock):
|
|
302
|
+
with raises(specimen.RegionError):
|
|
303
|
+
await region.wait_for("healthy", timeout=10)
|