]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/tests/test_object_format.py
b09df8ce384705861636d8edbbfda914c46679fa
[ceph.git] / ceph / src / pybind / mgr / tests / test_object_format.py
1 import errno
2 from typing import (
3 Any,
4 Dict,
5 Optional,
6 Tuple,
7 Type,
8 TypeVar,
9 )
10
11 import pytest
12
13 from mgr_module import CLICommand
14 import object_format
15
16
17 T = TypeVar("T", bound="Parent")
18
19
20 class Simpler:
21 def __init__(self, name, val=None):
22 self.name = name
23 self.val = val or {}
24 self.version = 1
25
26 def to_simplified(self) -> Dict[str, Any]:
27 return {
28 "version": self.version,
29 "name": self.name,
30 "value": self.val,
31 }
32
33
34 class JSONer(Simpler):
35 def to_json(self) -> Dict[str, Any]:
36 d = self.to_simplified()
37 d["_json"] = True
38 return d
39
40 @classmethod
41 def from_json(cls: Type[T], data) -> T:
42 o = cls(data.get("name", ""), data.get("value"))
43 o.version = data.get("version", 1) + 1
44 return o
45
46
47 class YAMLer(Simpler):
48 def to_yaml(self) -> Dict[str, Any]:
49 d = self.to_simplified()
50 d["_yaml"] = True
51 return d
52
53
54 @pytest.mark.parametrize(
55 "obj, compatible, json_val",
56 [
57 ({}, False, "{}"),
58 ({"name": "foobar"}, False, '{"name": "foobar"}'),
59 ([1, 2, 3], False, "[1, 2, 3]"),
60 (JSONer("bob"), False, '{"name": "bob", "value": {}, "version": 1}'),
61 (
62 JSONer("betty", 77),
63 False,
64 '{"name": "betty", "value": 77, "version": 1}',
65 ),
66 ({}, True, "{}"),
67 ({"name": "foobar"}, True, '{"name": "foobar"}'),
68 (
69 JSONer("bob"),
70 True,
71 '{"_json": true, "name": "bob", "value": {}, "version": 1}',
72 ),
73 ],
74 )
75 def test_format_json(obj: Any, compatible: bool, json_val: str):
76 assert (
77 object_format.ObjectFormatAdapter(
78 obj, compatible=compatible, json_indent=None
79 ).format_json()
80 == json_val
81 )
82
83
84 @pytest.mark.parametrize(
85 "obj, compatible, yaml_val",
86 [
87 ({}, False, "{}\n"),
88 ({"name": "foobar"}, False, "name: foobar\n"),
89 (
90 {"stuff": [1, 88, 909, 32]},
91 False,
92 "stuff:\n- 1\n- 88\n- 909\n- 32\n",
93 ),
94 (
95 JSONer("zebulon", "999"),
96 False,
97 "name: zebulon\nvalue: '999'\nversion: 1\n",
98 ),
99 ({}, True, "{}\n"),
100 ({"name": "foobar"}, True, "name: foobar\n"),
101 (
102 YAMLer("thingy", "404"),
103 True,
104 "_yaml: true\nname: thingy\nvalue: '404'\nversion: 1\n",
105 ),
106 ],
107 )
108 def test_format_yaml(obj: Any, compatible: bool, yaml_val: str):
109 assert (
110 object_format.ObjectFormatAdapter(
111 obj, compatible=compatible
112 ).format_yaml()
113 == yaml_val
114 )
115
116
117 class Retty:
118 def __init__(self, v) -> None:
119 self.value = v
120
121 def mgr_return_value(self) -> int:
122 return self.value
123
124
125 @pytest.mark.parametrize(
126 "obj, ret",
127 [
128 ({}, 0),
129 ({"fish": "sticks"}, 0),
130 (-55, 0),
131 (Retty(0), 0),
132 (Retty(-55), -55),
133 ],
134 )
135 def test_return_value(obj: Any, ret: int):
136 rva = object_format.ReturnValueAdapter(obj)
137 # a ReturnValueAdapter instance meets the ReturnValueProvider protocol.
138 assert object_format._is_return_value_provider(rva)
139 assert rva.mgr_return_value() == ret
140
141
142 def test_valid_formats():
143 ofa = object_format.ObjectFormatAdapter({"fred": "wilma"})
144 vf = ofa.valid_formats()
145 assert "json" in vf
146 assert "yaml" in vf
147 assert "xml" in vf
148 assert "plain" in vf
149
150
151 def test_error_response_exceptions():
152 err = object_format.ErrorResponseBase()
153 with pytest.raises(NotImplementedError):
154 err.format_response()
155
156 err = object_format.UnsupportedFormat("cheese")
157 assert err.format_response() == (-22, "", "Unsupported format: cheese")
158
159 err = object_format.UnknownFormat("chocolate")
160 assert err.format_response() == (-22, "", "Unknown format name: chocolate")
161
162
163 @pytest.mark.parametrize(
164 "value, format, result",
165 [
166 ({}, None, (0, "{}", "")),
167 ({"blat": True}, "json", (0, '{\n "blat": true\n}', "")),
168 ({"blat": True}, "yaml", (0, "blat: true\n", "")),
169 ({"blat": True}, "toml", (-22, "", "Unknown format name: toml")),
170 ({"blat": True}, "xml", (-22, "", "Unsupported format: xml")),
171 (
172 JSONer("hoop", "303"),
173 "yaml",
174 (0, "name: hoop\nvalue: '303'\nversion: 1\n", ""),
175 ),
176 ],
177 )
178 def test_responder_decorator_default(
179 value: Any, format: Optional[str], result: Tuple[int, str, str]
180 ) -> None:
181 @object_format.Responder()
182 def orf_value(format: Optional[str] = None):
183 return value
184
185 assert orf_value(format=format) == result
186
187
188 class PhonyMultiYAMLFormatAdapter(object_format.ObjectFormatAdapter):
189 """This adapter puts a yaml document/directive separator line
190 before all output. It doesn't actully support multiple documents.
191 """
192 def format_yaml(self):
193 yml = super().format_yaml()
194 return "---\n{}".format(yml)
195
196
197 @pytest.mark.parametrize(
198 "value, format, result",
199 [
200 ({}, None, (0, "{}", "")),
201 ({"blat": True}, "json", (0, '{\n "blat": true\n}', "")),
202 ({"blat": True}, "yaml", (0, "---\nblat: true\n", "")),
203 ({"blat": True}, "toml", (-22, "", "Unknown format name: toml")),
204 ({"blat": True}, "xml", (-22, "", "Unsupported format: xml")),
205 (
206 JSONer("hoop", "303"),
207 "yaml",
208 (0, "---\nname: hoop\nvalue: '303'\nversion: 1\n", ""),
209 ),
210 ],
211 )
212 def test_responder_decorator_custom(
213 value: Any, format: Optional[str], result: Tuple[int, str, str]
214 ) -> None:
215 @object_format.Responder(PhonyMultiYAMLFormatAdapter)
216 def orf_value(format: Optional[str] = None):
217 return value
218
219 assert orf_value(format=format) == result
220
221
222 class FancyDemoAdapter(PhonyMultiYAMLFormatAdapter):
223 """This adapter demonstrates adding formatting for other formats
224 like xml and plain text.
225 """
226 def format_xml(self) -> str:
227 name = self.obj.get("name")
228 size = self.obj.get("size")
229 return f'<object name="{name}" size="{size}" />'
230
231 def format_plain(self) -> str:
232 name = self.obj.get("name")
233 size = self.obj.get("size")
234 es = 'es' if size != 1 else ''
235 return f"{size} box{es} of {name}"
236
237
238 class DecoDemo:
239 """Class to stand in for a mgr module, used to test CLICommand integration."""
240
241 @CLICommand("alpha one", perm="rw")
242 @object_format.Responder()
243 def alpha_one(self, name: str = "default") -> Dict[str, str]:
244 return {
245 "alpha": "one",
246 "name": name,
247 "weight": 300,
248 }
249
250 @CLICommand("beta two", perm="r")
251 @object_format.Responder()
252 def beta_two(
253 self, name: str = "default", format: Optional[str] = None
254 ) -> Dict[str, str]:
255 return {
256 "beta": "two",
257 "name": name,
258 "weight": 72,
259 }
260
261 @CLICommand("gamma three", perm="rw")
262 @object_format.Responder(FancyDemoAdapter)
263 def gamma_three(self, size: int = 0) -> Dict[str, Any]:
264 return {"name": "funnystuff", "size": size}
265
266
267 @pytest.mark.parametrize(
268 "prefix, args, response",
269 [
270 (
271 "alpha one",
272 {"name": "moonbase"},
273 (
274 0,
275 '{\n "alpha": "one",\n "name": "moonbase",\n "weight": 300\n}',
276 "",
277 ),
278 ),
279 # ---
280 (
281 "alpha one",
282 {"name": "moonbase2", "format": "yaml"},
283 (
284 0,
285 "alpha: one\nname: moonbase2\nweight: 300\n",
286 "",
287 ),
288 ),
289 # ---
290 (
291 "alpha one",
292 {"name": "moonbase2", "format": "chocolate"},
293 (
294 -22,
295 "",
296 "Unknown format name: chocolate",
297 ),
298 ),
299 # ---
300 (
301 "beta two",
302 {"name": "blocker"},
303 (
304 0,
305 '{\n "beta": "two",\n "name": "blocker",\n "weight": 72\n}',
306 "",
307 ),
308 ),
309 # ---
310 (
311 "beta two",
312 {"name": "test", "format": "yaml"},
313 (
314 0,
315 "beta: two\nname: test\nweight: 72\n",
316 "",
317 ),
318 ),
319 # ---
320 (
321 "beta two",
322 {"name": "test", "format": "plain"},
323 (
324 -22,
325 "",
326 "Unsupported format: plain",
327 ),
328 ),
329 # ---
330 (
331 "gamma three",
332 {},
333 (
334 0,
335 '{\n "name": "funnystuff",\n "size": 0\n}',
336 "",
337 ),
338 ),
339 # ---
340 (
341 "gamma three",
342 {"size": 1, "format": "json"},
343 (
344 0,
345 '{\n "name": "funnystuff",\n "size": 1\n}',
346 "",
347 ),
348 ),
349 # ---
350 (
351 "gamma three",
352 {"size": 1, "format": "plain"},
353 (
354 0,
355 "1 box of funnystuff",
356 "",
357 ),
358 ),
359 # ---
360 (
361 "gamma three",
362 {"size": 2, "format": "plain"},
363 (
364 0,
365 "2 boxes of funnystuff",
366 "",
367 ),
368 ),
369 # ---
370 (
371 "gamma three",
372 {"size": 2, "format": "xml"},
373 (
374 0,
375 '<object name="funnystuff" size="2" />',
376 "",
377 ),
378 ),
379 # ---
380 (
381 "gamma three",
382 {"size": 2, "format": "toml"},
383 (
384 -22,
385 "",
386 "Unknown format name: toml",
387 ),
388 ),
389 ],
390 )
391 def test_cli_command_responder(prefix, args, response):
392 dd = DecoDemo()
393 assert CLICommand.COMMANDS[prefix].call(dd, args, None) == response
394
395
396 def test_error_response():
397 e1 = object_format.ErrorResponse("nope")
398 assert e1.format_response() == (-22, "", "nope")
399 assert e1.return_value == -22
400 assert e1.errno == 22
401 assert "ErrorResponse" in repr(e1)
402 assert "nope" in repr(e1)
403 assert e1.mgr_return_value() == -22
404
405 try:
406 open("/this/is_/extremely_/unlikely/_to/exist.txt")
407 except Exception as e:
408 e2 = object_format.ErrorResponse.wrap(e)
409 r = e2.format_response()
410 assert r[0] == -errno.ENOENT
411 assert r[1] == ""
412 assert "No such file or directory" in r[2]
413 assert "ErrorResponse" in repr(e2)
414 assert "No such file or directory" in repr(e2)
415 assert r[0] == e2.mgr_return_value()
416
417 e3 = object_format.ErrorResponse.wrap(RuntimeError("blat"))
418 r = e3.format_response()
419 assert r[0] == -errno.EINVAL
420 assert r[1] == ""
421 assert "blat" in r[2]
422 assert r[0] == e3.mgr_return_value()