ceph-devstack 0.1.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.
- ceph_devstack/Dockerfile.selinux +20 -0
- ceph_devstack/__init__.py +187 -0
- ceph_devstack/ceph_devstack.pp +0 -0
- ceph_devstack/ceph_devstack.te +127 -0
- ceph_devstack/cli.py +64 -0
- ceph_devstack/config.toml +24 -0
- ceph_devstack/exec.py +93 -0
- ceph_devstack/host.py +154 -0
- ceph_devstack/logging.conf +30 -0
- ceph_devstack/py.typed +0 -0
- ceph_devstack/requirements.py +277 -0
- ceph_devstack/resources/__init__.py +115 -0
- ceph_devstack/resources/ceph/__init__.py +266 -0
- ceph_devstack/resources/ceph/containers.py +419 -0
- ceph_devstack/resources/ceph/exceptions.py +3 -0
- ceph_devstack/resources/ceph/requirements.py +90 -0
- ceph_devstack/resources/ceph/utils.py +45 -0
- ceph_devstack/resources/container.py +171 -0
- ceph_devstack/resources/misc.py +15 -0
- ceph_devstack-0.1.0.dist-info/METADATA +222 -0
- ceph_devstack-0.1.0.dist-info/RECORD +44 -0
- ceph_devstack-0.1.0.dist-info/WHEEL +5 -0
- ceph_devstack-0.1.0.dist-info/entry_points.txt +2 -0
- ceph_devstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- ceph_devstack-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +9 -0
- tests/resources/__init__.py +0 -0
- tests/resources/ceph/__init__.py +0 -0
- tests/resources/ceph/fixtures/__init__.py +0 -0
- tests/resources/ceph/fixtures/testnode-config.toml +2 -0
- tests/resources/ceph/test_cephdevstack_core.py +459 -0
- tests/resources/ceph/test_devstack.py +182 -0
- tests/resources/ceph/test_env_vars.py +110 -0
- tests/resources/ceph/test_requirements_ceph.py +262 -0
- tests/resources/ceph/test_ssh_keypair.py +109 -0
- tests/resources/ceph/test_testnode.py +36 -0
- tests/resources/test_container.py +247 -0
- tests/resources/test_misc.py +46 -0
- tests/resources/test_podmanresource.py +59 -0
- tests/test_config.py +120 -0
- tests/test_deep_merge.py +71 -0
- tests/test_parse_args.py +228 -0
- tests/test_requirements_core.py +495 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch, AsyncMock
|
|
7
|
+
|
|
8
|
+
from ceph_devstack import config
|
|
9
|
+
from ceph_devstack.resources.container import Container
|
|
10
|
+
from .test_podmanresource import (
|
|
11
|
+
TestPodmanResource as _TestPodmanResource,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _TestContainerBase:
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def cls(self):
|
|
18
|
+
return Container
|
|
19
|
+
|
|
20
|
+
def setup_method(self):
|
|
21
|
+
config["containers"]["container"] = {"image": "example.com/image:latest"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestContainerResource(_TestPodmanResource, _TestContainerBase):
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def cls(self):
|
|
27
|
+
return Container
|
|
28
|
+
|
|
29
|
+
@pytest.fixture(
|
|
30
|
+
scope="class", params=["build", "create", "start", "stop", "remove"]
|
|
31
|
+
)
|
|
32
|
+
def action(self, request):
|
|
33
|
+
return request.param
|
|
34
|
+
|
|
35
|
+
async def test_action_calls_cmd_with_correct_args(self, cls, action):
|
|
36
|
+
if action == "build":
|
|
37
|
+
config["containers"][cls.__name__.lower()]["repo"] = "/repo_path"
|
|
38
|
+
obj = cls()
|
|
39
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
40
|
+
mock_proc = AsyncMock()
|
|
41
|
+
mock_cmd.return_value = mock_proc
|
|
42
|
+
action_cmd = "rm" if action == "remove" else action
|
|
43
|
+
await getattr(obj, action)()
|
|
44
|
+
if action == "create":
|
|
45
|
+
assert len(mock_cmd.call_args_list) == 2
|
|
46
|
+
assert "inspect" in mock_cmd.call_args_list[0][0][0]
|
|
47
|
+
assert action_cmd in mock_cmd.call_args_list[-1][0][0]
|
|
48
|
+
else:
|
|
49
|
+
mock_cmd.assert_called_once()
|
|
50
|
+
call_args = mock_cmd.call_args[0][0]
|
|
51
|
+
assert action_cmd in call_args
|
|
52
|
+
|
|
53
|
+
async def test_empty_cmd_skips_action(self, cls, action):
|
|
54
|
+
with patch.object(cls, "cmd"):
|
|
55
|
+
obj = cls()
|
|
56
|
+
setattr(obj, f"{action}_cmd", [])
|
|
57
|
+
await getattr(obj, action)()
|
|
58
|
+
obj.cmd.assert_not_awaited()
|
|
59
|
+
|
|
60
|
+
async def test_action_cmd_called_with_stream_output(self, cls, action):
|
|
61
|
+
if action == "remove":
|
|
62
|
+
pytest.skip("remove action doesn't stream output")
|
|
63
|
+
if action == "build":
|
|
64
|
+
config["containers"][cls.__name__.lower()]["repo"] = "/repo_path"
|
|
65
|
+
with patch.object(cls, "cmd") as mock_cmd:
|
|
66
|
+
obj = cls()
|
|
67
|
+
await getattr(obj, action)()
|
|
68
|
+
_, kwargs = mock_cmd.call_args
|
|
69
|
+
assert kwargs.get("stream_output") is True
|
|
70
|
+
|
|
71
|
+
async def test_build_action_skips_when_no_repo(self, cls):
|
|
72
|
+
config["containers"][cls.__name__.lower()]["repo"] = ""
|
|
73
|
+
obj = cls()
|
|
74
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
75
|
+
await obj.build()
|
|
76
|
+
mock_cmd.assert_not_called()
|
|
77
|
+
|
|
78
|
+
async def test_pull_action_skips_localhost_images(self, cls):
|
|
79
|
+
config["containers"]["container"]["image"] = "localhost/image:latest"
|
|
80
|
+
obj = cls()
|
|
81
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
82
|
+
await obj.pull()
|
|
83
|
+
mock_cmd.assert_not_called()
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize(
|
|
86
|
+
"output,rc,expected", ([b"12345", 0, 12345], [b"error", 1, 1])
|
|
87
|
+
)
|
|
88
|
+
async def test_wait_returns_output_on_success(self, cls, output, rc, expected):
|
|
89
|
+
obj = cls()
|
|
90
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
91
|
+
mock_proc = AsyncMock()
|
|
92
|
+
mock_proc.communicate = AsyncMock(return_value=(output, b""))
|
|
93
|
+
mock_proc.returncode = rc
|
|
94
|
+
mock_cmd.return_value = mock_proc
|
|
95
|
+
result = await obj.wait()
|
|
96
|
+
assert expected == result
|
|
97
|
+
|
|
98
|
+
async def test_wait_action_returns_error_code_on_failure(self, cls):
|
|
99
|
+
obj = cls()
|
|
100
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
101
|
+
mock_proc = AsyncMock()
|
|
102
|
+
mock_proc.communicate = AsyncMock(return_value=(b"", b"error occurred"))
|
|
103
|
+
mock_proc.returncode = 130
|
|
104
|
+
mock_cmd.return_value = mock_proc
|
|
105
|
+
result = await obj.wait()
|
|
106
|
+
assert result == 130
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestContainerInit(_TestContainerBase):
|
|
110
|
+
def test_init_sets_env_vars_from_class(self, cls):
|
|
111
|
+
Container.env_vars = {"TEST_VAR": "default_value"}
|
|
112
|
+
obj = cls()
|
|
113
|
+
assert "TEST_VAR" in obj.env_vars
|
|
114
|
+
assert obj.env_vars["TEST_VAR"] == "default_value"
|
|
115
|
+
|
|
116
|
+
def test_init_overrides_env_vars_from_environment(self, cls):
|
|
117
|
+
Container.env_vars = {"TEST_VAR": "default"}
|
|
118
|
+
with patch.dict(os.environ, {"TEST_VAR": "env_value"}):
|
|
119
|
+
obj = cls()
|
|
120
|
+
assert obj.env_vars["TEST_VAR"] == "env_value"
|
|
121
|
+
|
|
122
|
+
def test_init_does_not_override_missing_env_vars(self, cls):
|
|
123
|
+
Container.env_vars = {"TEST_VAR": "default"}
|
|
124
|
+
with patch.dict(os.environ, {}, clear=False):
|
|
125
|
+
if "TEST_VAR" in os.environ:
|
|
126
|
+
del os.environ["TEST_VAR"]
|
|
127
|
+
obj = cls()
|
|
128
|
+
assert obj.env_vars["TEST_VAR"] == "default"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestContainerExists(_TestContainerBase):
|
|
132
|
+
@pytest.mark.parametrize("rc,res", ([0, True], [1, False]))
|
|
133
|
+
async def test_exists(self, cls, rc, res):
|
|
134
|
+
with patch.object(cls, "cmd"):
|
|
135
|
+
obj = cls()
|
|
136
|
+
obj.cmd.return_value = AsyncMock()
|
|
137
|
+
obj.cmd.return_value.wait.return_value = rc
|
|
138
|
+
assert await obj.exists() == res
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestContainerRunning(_TestContainerBase):
|
|
142
|
+
async def test_is_running_yes(self, cls):
|
|
143
|
+
with patch.object(cls, "cmd"):
|
|
144
|
+
obj = cls()
|
|
145
|
+
output_obj = [{"State": {"Status": "running"}}]
|
|
146
|
+
m_read = AsyncMock(return_value=json.dumps(output_obj))
|
|
147
|
+
m_stdout = AsyncMock(read=m_read)
|
|
148
|
+
obj.cmd.return_value = AsyncMock(
|
|
149
|
+
stdout=m_stdout,
|
|
150
|
+
returncode=0,
|
|
151
|
+
)
|
|
152
|
+
obj.cmd.return_value.wait.return_value = 0
|
|
153
|
+
assert await obj.is_running() is True
|
|
154
|
+
|
|
155
|
+
async def test_is_running_no_bc_status(self, cls):
|
|
156
|
+
with patch.object(cls, "cmd"):
|
|
157
|
+
obj = cls()
|
|
158
|
+
output_obj = [{"State": {"Status": "crashed"}}]
|
|
159
|
+
m_read = AsyncMock(return_value=json.dumps(output_obj))
|
|
160
|
+
m_stdout = AsyncMock(read=m_read)
|
|
161
|
+
obj.cmd.return_value = AsyncMock(
|
|
162
|
+
stdout=m_stdout,
|
|
163
|
+
returncode=0,
|
|
164
|
+
)
|
|
165
|
+
obj.cmd.return_value.wait.return_value = 0
|
|
166
|
+
assert await obj.is_running() is False
|
|
167
|
+
|
|
168
|
+
async def test_is_running_no_bc_dne(self, cls):
|
|
169
|
+
with patch.object(cls, "cmd"):
|
|
170
|
+
obj = cls()
|
|
171
|
+
obj.cmd.return_value = AsyncMock(returncode=1)
|
|
172
|
+
assert await obj.is_running() is False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestContainerImageName(_TestContainerBase):
|
|
176
|
+
def test_image_name_default_returns_class_name(self, cls):
|
|
177
|
+
obj = cls()
|
|
178
|
+
assert obj.image_name == "container"
|
|
179
|
+
|
|
180
|
+
def test_image_name_returns_custom_when_set(self, cls):
|
|
181
|
+
obj = cls()
|
|
182
|
+
obj._image_name = "custom-image"
|
|
183
|
+
assert obj.image_name == "custom-image"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestContainerImageTag(_TestContainerBase):
|
|
187
|
+
def test_image_tag_with_colon(self, cls):
|
|
188
|
+
obj = cls()
|
|
189
|
+
config["containers"]["container"] = {"image": "example.com/image:v1.0"}
|
|
190
|
+
assert obj.image_tag == "v1.0"
|
|
191
|
+
|
|
192
|
+
def test_image_tag_without_colon(self, cls):
|
|
193
|
+
obj = cls()
|
|
194
|
+
config["containers"]["container"] = {"image": "example.com/image"}
|
|
195
|
+
assert obj.image_tag == "latest"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestContainerImage(_TestContainerBase):
|
|
199
|
+
def test_image_returns_config_image_when_no_repo(self, cls):
|
|
200
|
+
obj = cls()
|
|
201
|
+
assert obj.image == "example.com/image:latest"
|
|
202
|
+
|
|
203
|
+
def test_image_returns_localhost_when_repo_exists(self, cls):
|
|
204
|
+
config["containers"]["container"]["repo"] = "/path/to/repo"
|
|
205
|
+
obj = cls()
|
|
206
|
+
obj._image_name = "my-image"
|
|
207
|
+
assert obj.image == "localhost/my-image"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestContainerCwd(_TestContainerBase):
|
|
211
|
+
def test_cwd_returns_repo_when_exists(self, cls):
|
|
212
|
+
obj = cls()
|
|
213
|
+
with patch.object(type(obj), "repo", Path("/path/to/repo")):
|
|
214
|
+
assert obj.cwd == Path("/path/to/repo")
|
|
215
|
+
|
|
216
|
+
def test_cwd_returns_dot_when_no_repo(self, cls):
|
|
217
|
+
obj = cls()
|
|
218
|
+
assert obj.cwd == "."
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestContainerAddEnvToArgs(_TestContainerBase):
|
|
222
|
+
def test_add_env_to_args_inserts_env_vars(self, cls):
|
|
223
|
+
obj = cls()
|
|
224
|
+
obj.env_vars = {"KEY1": "value1", "KEY2": "value2"}
|
|
225
|
+
args = ["podman", "run", "image"]
|
|
226
|
+
result = obj.add_env_to_args(args)
|
|
227
|
+
assert result[-1] == "image" # last element is preserved
|
|
228
|
+
assert "-e" in result
|
|
229
|
+
assert "KEY1=value1" in result
|
|
230
|
+
assert "KEY2=value2" in result
|
|
231
|
+
|
|
232
|
+
def test_add_env_to_args_skips_empty_values(self, cls):
|
|
233
|
+
obj = cls()
|
|
234
|
+
obj.env_vars = {"KEY1": "value1", "KEY2": None, "KEY3": ""}
|
|
235
|
+
args = ["podman", "run", "image"]
|
|
236
|
+
result = obj.add_env_to_args(args)
|
|
237
|
+
assert "KEY1=value1" in result
|
|
238
|
+
assert "KEY2=" not in result
|
|
239
|
+
assert "KEY3=" not in result
|
|
240
|
+
|
|
241
|
+
def test_add_env_to_args_preserves_order(self, cls):
|
|
242
|
+
obj = cls()
|
|
243
|
+
obj.env_vars = {"KEY": "value"}
|
|
244
|
+
args = ["podman", "run", "image"]
|
|
245
|
+
result = obj.add_env_to_args(args)
|
|
246
|
+
assert result[-1] == "image"
|
|
247
|
+
assert result.index("-e") < result.index("image")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch, AsyncMock
|
|
4
|
+
|
|
5
|
+
from ceph_devstack.resources.misc import Network, Secret
|
|
6
|
+
|
|
7
|
+
from .test_podmanresource import (
|
|
8
|
+
TestPodmanResource as _TestPodmanResource,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestMiscResource(_TestPodmanResource):
|
|
13
|
+
@pytest.fixture(scope="class", params=[Network, Secret])
|
|
14
|
+
def cls(self, request):
|
|
15
|
+
return request.param
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(scope="class", params=["create", "exists", "remove"])
|
|
18
|
+
def action(self, request):
|
|
19
|
+
return request.param
|
|
20
|
+
|
|
21
|
+
async def test_exists_means_inspect(self, cls):
|
|
22
|
+
obj = cls()
|
|
23
|
+
assert "inspect" in obj.exists_cmd
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize("rc,expected", [[0, True], [1, False]])
|
|
26
|
+
async def test_exists(self, cls, rc, expected):
|
|
27
|
+
obj = cls()
|
|
28
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
29
|
+
mock_proc = AsyncMock()
|
|
30
|
+
mock_proc.wait = AsyncMock(return_value=rc)
|
|
31
|
+
mock_cmd.return_value = mock_proc
|
|
32
|
+
result = await obj.exists()
|
|
33
|
+
assert result is expected
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("exists", [True, False])
|
|
36
|
+
async def test_create_when_not_exists(self, cls, exists):
|
|
37
|
+
obj = cls()
|
|
38
|
+
with (
|
|
39
|
+
patch.object(obj, "exists", return_value=exists),
|
|
40
|
+
patch.object(obj, "cmd") as mock_cmd,
|
|
41
|
+
):
|
|
42
|
+
mock_proc = AsyncMock()
|
|
43
|
+
mock_proc.wait = AsyncMock(return_value=(0 if exists else 1))
|
|
44
|
+
mock_cmd.return_value = mock_proc
|
|
45
|
+
await obj.create()
|
|
46
|
+
assert len(mock_cmd.call_args_list) == (0 if exists else 1)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from subprocess import CalledProcessError
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from ceph_devstack.resources import PodmanResource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestPodmanResource:
|
|
12
|
+
@pytest.fixture(scope="class")
|
|
13
|
+
def cls(self):
|
|
14
|
+
return PodmanResource
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(scope="class", params=["create", "remove"])
|
|
17
|
+
def action(self, request):
|
|
18
|
+
return request.param
|
|
19
|
+
|
|
20
|
+
def test_name(self, cls):
|
|
21
|
+
obj = cls()
|
|
22
|
+
assert obj.name == cls.__name__.lower()
|
|
23
|
+
obj = cls(name="foo")
|
|
24
|
+
assert obj.name == "foo"
|
|
25
|
+
|
|
26
|
+
def test_format_cmd(self, cls):
|
|
27
|
+
obj = cls(name="pr")
|
|
28
|
+
assert "name" in obj.cmd_vars
|
|
29
|
+
res = obj.format_cmd(["foo", "{name}", "bar", "x{name}x"])
|
|
30
|
+
assert res == ["foo", "pr", "bar", "xprx"]
|
|
31
|
+
|
|
32
|
+
def test_cmd_vars_contains_name(self, cls):
|
|
33
|
+
assert "name" in cls.cmd_vars
|
|
34
|
+
|
|
35
|
+
def test_repr(self, cls):
|
|
36
|
+
obj = cls()
|
|
37
|
+
class_name = cls.__name__
|
|
38
|
+
assert repr(obj) == f"{class_name}()"
|
|
39
|
+
obj = cls(name="foo")
|
|
40
|
+
assert repr(obj) == f'{class_name}(name="foo")'
|
|
41
|
+
|
|
42
|
+
async def test_apply(self, cls, action):
|
|
43
|
+
with patch.object(cls, action):
|
|
44
|
+
obj = cls()
|
|
45
|
+
await obj.apply(action)
|
|
46
|
+
method = getattr(obj, action)
|
|
47
|
+
method.assert_awaited_once()
|
|
48
|
+
|
|
49
|
+
async def test_cmd(self, cls):
|
|
50
|
+
with patch("ceph_devstack.host.host.arun") as m_arun:
|
|
51
|
+
obj = cls()
|
|
52
|
+
await obj.cmd(["0"])
|
|
53
|
+
print(m_arun.await_args_list)
|
|
54
|
+
m_arun.assert_awaited_once_with(["0"], cwd=Path("."), stream_output=False)
|
|
55
|
+
|
|
56
|
+
async def test_cmd_failed(self, cls):
|
|
57
|
+
obj = cls()
|
|
58
|
+
with pytest.raises(CalledProcessError):
|
|
59
|
+
await obj.cmd(["false"], check=True)
|
tests/test_config.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import tomlkit
|
|
2
|
+
|
|
3
|
+
from ceph_devstack import config, Config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestConfigDump:
|
|
7
|
+
def test_config_dump_returns_string(self):
|
|
8
|
+
result = config.dump()
|
|
9
|
+
assert isinstance(result, str)
|
|
10
|
+
|
|
11
|
+
def test_config_dump_is_valid_toml(self):
|
|
12
|
+
result = config.dump()
|
|
13
|
+
parsed = tomlkit.parse(result)
|
|
14
|
+
assert isinstance(parsed, tomlkit.TOMLDocument)
|
|
15
|
+
|
|
16
|
+
def test_config_contents_basic(self):
|
|
17
|
+
result = config.dump()
|
|
18
|
+
requires = [
|
|
19
|
+
"containers",
|
|
20
|
+
"data_dir",
|
|
21
|
+
"postgres",
|
|
22
|
+
"beanstalk",
|
|
23
|
+
"paddles",
|
|
24
|
+
"pulpito",
|
|
25
|
+
"testnode",
|
|
26
|
+
"teuthology",
|
|
27
|
+
"archive",
|
|
28
|
+
]
|
|
29
|
+
for item in requires:
|
|
30
|
+
assert item in result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestConfigGetValue:
|
|
34
|
+
def test_get_value_simple_key(self):
|
|
35
|
+
result = config.get_value("data_dir")
|
|
36
|
+
assert isinstance(result, str)
|
|
37
|
+
|
|
38
|
+
def test_get_value_nested_count(self):
|
|
39
|
+
result = config.get_value("containers.testnode.count")
|
|
40
|
+
assert result == "3"
|
|
41
|
+
|
|
42
|
+
def test_get_value_nested_loop_device_size(self):
|
|
43
|
+
result = config.get_value("containers.testnode.loop_device_size")
|
|
44
|
+
assert result == "5G"
|
|
45
|
+
|
|
46
|
+
def test_get_value_nested_image(self):
|
|
47
|
+
result = config.get_value("containers.testnode.image")
|
|
48
|
+
assert "quay.io/ceph-infra/teuthology-testnode:main" in result
|
|
49
|
+
|
|
50
|
+
def test_get_value_returns_string_for_int(self):
|
|
51
|
+
result = config.get_value("containers.testnode.count")
|
|
52
|
+
assert result == "3"
|
|
53
|
+
assert isinstance(result, str)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestConfigSet:
|
|
57
|
+
def test_set_value_simple_key(self, tmp_path):
|
|
58
|
+
test_config = Config()
|
|
59
|
+
config_file = tmp_path / "test_config.toml"
|
|
60
|
+
config_file.write_text("")
|
|
61
|
+
test_config.load(config_file)
|
|
62
|
+
test_config.set_value("test_key", "test_value")
|
|
63
|
+
assert test_config["test_key"] == "test_value"
|
|
64
|
+
|
|
65
|
+
def test_set_value_nested_key(self, tmp_path):
|
|
66
|
+
test_config = Config()
|
|
67
|
+
config_file = tmp_path / "test_config.toml"
|
|
68
|
+
config_file.write_text("")
|
|
69
|
+
test_config.load(config_file)
|
|
70
|
+
test_config.set_value("test_section.test_key", "test_value")
|
|
71
|
+
assert test_config["test_section"]["test_key"] == "test_value"
|
|
72
|
+
|
|
73
|
+
def test_set_value_updates_user_obj(self, tmp_path):
|
|
74
|
+
test_config = Config()
|
|
75
|
+
config_file = tmp_path / "test_config.toml"
|
|
76
|
+
config_file.write_text("")
|
|
77
|
+
test_config.load(config_file)
|
|
78
|
+
test_config.set_value("new_key", "new_value")
|
|
79
|
+
assert "new_key" in test_config.user_obj
|
|
80
|
+
|
|
81
|
+
def test_set_value_creates_intermediate_sections(self, tmp_path):
|
|
82
|
+
test_config = Config()
|
|
83
|
+
config_file = tmp_path / "test_config.toml"
|
|
84
|
+
config_file.write_text("")
|
|
85
|
+
test_config.load(config_file)
|
|
86
|
+
test_config.set_value("deep.nested.key", "value")
|
|
87
|
+
assert test_config.user_obj["deep"]["nested"]["key"] == "value"
|
|
88
|
+
|
|
89
|
+
def test_set_value_overrides_existing(self, tmp_path):
|
|
90
|
+
test_config = Config()
|
|
91
|
+
config_file = tmp_path / "test_config.toml"
|
|
92
|
+
config_file.write_text("")
|
|
93
|
+
test_config.load(config_file)
|
|
94
|
+
original_count = test_config["containers"]["testnode"]["count"]
|
|
95
|
+
new_count = original_count + 2
|
|
96
|
+
test_config.set_value("containers.testnode.count", str(new_count))
|
|
97
|
+
assert test_config["containers"]["testnode"]["count"] != original_count
|
|
98
|
+
assert test_config["containers"]["testnode"]["count"] == new_count
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestConfigDefaults:
|
|
102
|
+
def test_config_defaults(self):
|
|
103
|
+
assert config == {
|
|
104
|
+
"data_dir": "~/.local/share/ceph-devstack",
|
|
105
|
+
"containers": {
|
|
106
|
+
"archive": {"image": "python:alpine"},
|
|
107
|
+
"beanstalk": {"image": "quay.io/ceph-infra/teuthology-beanstalkd:main"},
|
|
108
|
+
"paddles": {"image": "quay.io/ceph-infra/paddles:main"},
|
|
109
|
+
"postgres": {
|
|
110
|
+
"image": "quay.io/ceph-infra/teuthology-postgresql:latest"
|
|
111
|
+
},
|
|
112
|
+
"pulpito": {"image": "quay.io/ceph-infra/pulpito:main"},
|
|
113
|
+
"testnode": {
|
|
114
|
+
"count": 3,
|
|
115
|
+
"loop_device_size": "5G",
|
|
116
|
+
"image": "quay.io/ceph-infra/teuthology-testnode:main",
|
|
117
|
+
},
|
|
118
|
+
"teuthology": {"image": "quay.io/ceph-infra/teuthology-dev:main"},
|
|
119
|
+
},
|
|
120
|
+
}
|
tests/test_deep_merge.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from ceph_devstack import deep_merge
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestDeepMerge:
|
|
5
|
+
def test_deep_merge_empty_maps(self):
|
|
6
|
+
result = deep_merge()
|
|
7
|
+
assert result == {}
|
|
8
|
+
|
|
9
|
+
def test_deep_merge_single_map(self):
|
|
10
|
+
m = {"a": 1, "b": 2}
|
|
11
|
+
result = deep_merge(m)
|
|
12
|
+
assert result == m
|
|
13
|
+
|
|
14
|
+
def test_deep_merge_two_maps_no_overlap(self):
|
|
15
|
+
m1 = {"a": 1}
|
|
16
|
+
m2 = {"b": 2}
|
|
17
|
+
result = deep_merge(m1, m2)
|
|
18
|
+
assert result == {"a": 1, "b": 2}
|
|
19
|
+
|
|
20
|
+
def test_deep_merge_two_maps_with_overlap(self):
|
|
21
|
+
m1 = {"a": 1, "b": 2}
|
|
22
|
+
m2 = {"b": 3, "c": 4}
|
|
23
|
+
result = deep_merge(m1, m2)
|
|
24
|
+
assert result == {"a": 1, "b": 3, "c": 4}
|
|
25
|
+
|
|
26
|
+
def test_deep_merge_nested_dicts(self):
|
|
27
|
+
m1 = {"a": {"x": 1, "y": 2}}
|
|
28
|
+
m2 = {"a": {"y": 3, "z": 4}}
|
|
29
|
+
result = deep_merge(m1, m2)
|
|
30
|
+
assert result == {"a": {"x": 1, "y": 3, "z": 4}}
|
|
31
|
+
|
|
32
|
+
def test_deep_merge_three_maps(self):
|
|
33
|
+
m1 = {"a": 1}
|
|
34
|
+
m2 = {"b": 2}
|
|
35
|
+
m3 = {"c": 3}
|
|
36
|
+
result = deep_merge(m1, m2, m3)
|
|
37
|
+
assert result == {"a": 1, "b": 2, "c": 3}
|
|
38
|
+
|
|
39
|
+
def test_deep_merge_nested_override(self):
|
|
40
|
+
m1 = {"outer": {"inner": "default", "keep": "value"}}
|
|
41
|
+
m2 = {"outer": {"inner": "override"}}
|
|
42
|
+
result = deep_merge(m1, m2)
|
|
43
|
+
assert result["outer"]["inner"] == "override"
|
|
44
|
+
assert result["outer"]["keep"] == "value"
|
|
45
|
+
|
|
46
|
+
def test_deep_merge_with_none_value(self):
|
|
47
|
+
m1 = {"a": 1}
|
|
48
|
+
m2 = {"b": None}
|
|
49
|
+
result = deep_merge(m1, m2)
|
|
50
|
+
assert result == {"a": 1, "b": None}
|
|
51
|
+
|
|
52
|
+
def test_deep_merge_with_list_values(self):
|
|
53
|
+
m1 = {"a": [1, 2, 3]}
|
|
54
|
+
m2 = {"a": [4, 5]}
|
|
55
|
+
result = deep_merge(m1, m2)
|
|
56
|
+
assert result["a"] == [4, 5]
|
|
57
|
+
|
|
58
|
+
def test_deep_merge_does_not_modify_original_maps(self):
|
|
59
|
+
m1 = {"a": {"x": 1}}
|
|
60
|
+
m2 = {"a": {"y": 2}}
|
|
61
|
+
m1_copy = {"a": {"x": 1}}
|
|
62
|
+
m2_copy = {"a": {"y": 2}}
|
|
63
|
+
deep_merge(m1, m2)
|
|
64
|
+
assert m1 == m1_copy
|
|
65
|
+
assert m2 == m2_copy
|
|
66
|
+
|
|
67
|
+
def test_deep_merge_with_different_types(self):
|
|
68
|
+
m1 = {"a": 1}
|
|
69
|
+
m2 = {"a": "string"}
|
|
70
|
+
result = deep_merge(m1, m2)
|
|
71
|
+
assert result["a"] == "string"
|