amd-debug-tools 0.2.0__py3-none-any.whl → 0.2.2__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.
test_common.py ADDED
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ This module contains unit tests for the common functions in the amd-debug-tools package.
6
+ """
7
+ from unittest.mock import patch, mock_open, call
8
+
9
+ import logging
10
+ import tempfile
11
+ import unittest
12
+ import os
13
+ from platform import uname_result
14
+
15
+
16
+ from amd_debug.common import (
17
+ apply_prefix_wrapper,
18
+ Colors,
19
+ convert_string_to_bool,
20
+ colorize_choices,
21
+ check_lockdown,
22
+ compare_file,
23
+ find_ip_version,
24
+ fatal_error,
25
+ get_distro,
26
+ get_log_priority,
27
+ get_pretty_distro,
28
+ is_root,
29
+ minimum_kernel,
30
+ print_color,
31
+ run_countdown,
32
+ systemd_in_use,
33
+ running_ssh,
34
+ )
35
+
36
+ color_dict = {
37
+ "🚦": Colors.WARNING,
38
+ "🦟": Colors.DEBUG,
39
+ "❌": Colors.FAIL,
40
+ "👀": Colors.FAIL,
41
+ "✅": Colors.OK,
42
+ "🔋": Colors.OK,
43
+ "🐧": Colors.OK,
44
+ "💻": Colors.OK,
45
+ "○": Colors.OK,
46
+ "💤": Colors.OK,
47
+ "💯": Colors.UNDERLINE,
48
+ "🗣️": Colors.HEADER,
49
+ }
50
+
51
+
52
+ class TestCommon(unittest.TestCase):
53
+ """Test common functions"""
54
+
55
+ @classmethod
56
+ def setUpClass(cls):
57
+ logging.basicConfig(filename="/dev/null", level=logging.DEBUG)
58
+
59
+ def test_read_compare_file(self):
60
+ """Test read_file and compare_file strip files correctly"""
61
+
62
+ f = tempfile.NamedTemporaryFile()
63
+ f.write("foo bar baz\n ".encode("utf-8"))
64
+ f.seek(0)
65
+ self.assertTrue(compare_file(f.name, "foo bar baz"))
66
+
67
+ def test_countdown(self):
68
+ """Test countdown function"""
69
+
70
+ result = run_countdown("Full foo", 1)
71
+ self.assertTrue(result)
72
+ result = run_countdown("Half foo", 0.5)
73
+ self.assertTrue(result)
74
+ result = run_countdown("No foo", 0)
75
+ self.assertTrue(result)
76
+ result = run_countdown("Negative foo", -1)
77
+ self.assertFalse(result)
78
+
79
+ @patch("os.path.exists", return_value=True)
80
+ @patch("builtins.open", new_callable=mock_open, read_data="ID=foo\nVERSION_ID=bar")
81
+ def test_get_distro_known(self, mock_exists, _mock_open):
82
+ """Test get_distro function"""
83
+ distro = get_distro()
84
+ mock_exists.assert_has_calls(
85
+ [
86
+ call("/etc/os-release", "r", encoding="utf-8"),
87
+ call().__enter__(),
88
+ call().__iter__(),
89
+ call().__exit__(None, None, None),
90
+ ]
91
+ )
92
+ self.assertEqual(distro, "foo")
93
+
94
+ @patch("os.path.exists", return_value=False)
95
+ def test_get_distro_unknown(self, mock_exists):
96
+ """Test get_distro function"""
97
+ distro = get_distro()
98
+ mock_exists.assert_has_calls(
99
+ [
100
+ call("/etc/os-release"),
101
+ call("/etc/arch-release"),
102
+ call("/etc/fedora-release"),
103
+ call("/etc/debian_version"),
104
+ ]
105
+ )
106
+ self.assertEqual(distro, "unknown")
107
+
108
+ @patch("os.path.exists", return_value=True)
109
+ @patch("builtins.open", new_callable=mock_open, read_data="PRETTY_NAME=Foo")
110
+ def test_get_pretty_distro_known(self, mock_exists, _mock_open):
111
+ """Test get_distro function"""
112
+ distro = get_pretty_distro()
113
+ self.assertEqual(distro, "Foo")
114
+ mock_exists.assert_has_calls(
115
+ [
116
+ call("/etc/os-release", "r", encoding="utf-8"),
117
+ call().__enter__(),
118
+ call().__iter__(),
119
+ call().__exit__(None, None, None),
120
+ ]
121
+ )
122
+
123
+ @patch("os.path.exists", return_value=False)
124
+ def test_get_pretty_distro_unknown(self, mock_exists):
125
+ """Test get_distro function"""
126
+ distro = get_pretty_distro()
127
+ self.assertEqual(distro, "Unknown")
128
+ mock_exists.assert_has_calls(
129
+ [
130
+ call("/etc/os-release"),
131
+ ]
132
+ )
133
+
134
+ @patch("os.path.exists", return_value=True)
135
+ @patch(
136
+ "builtins.open",
137
+ new_callable=mock_open,
138
+ read_data="[none] integrity confidentiality",
139
+ )
140
+ def test_lockdown_pass(self, mock_exists, _mock_file):
141
+ """Test lockdown function"""
142
+ lockdown = check_lockdown()
143
+ mock_exists.assert_called_once_with(
144
+ "/sys/kernel/security/lockdown", "r", encoding="utf-8"
145
+ )
146
+ self.assertFalse(lockdown)
147
+
148
+ @patch("os.path.exists", return_value=True)
149
+ @patch(
150
+ "builtins.open",
151
+ new_callable=mock_open,
152
+ read_data="none [integrity] confidentiality",
153
+ )
154
+ def test_lockdown_fail_integrity(self, mock_exists, _mock_file):
155
+ """Test lockdown function"""
156
+ lockdown = check_lockdown()
157
+ mock_exists.assert_called_once_with(
158
+ "/sys/kernel/security/lockdown", "r", encoding="utf-8"
159
+ )
160
+ self.assertTrue(lockdown)
161
+
162
+ @patch("os.path.exists", return_value=True)
163
+ @patch(
164
+ "builtins.open",
165
+ new_callable=mock_open,
166
+ read_data="none integrity [confidentiality]",
167
+ )
168
+ def test_lockdown_fail_confidentiality(self, mock_exists, _mock_file):
169
+ """Test lockdown function"""
170
+ lockdown = check_lockdown()
171
+ mock_exists.assert_called_once_with(
172
+ "/sys/kernel/security/lockdown", "r", encoding="utf-8"
173
+ )
174
+ self.assertTrue(lockdown)
175
+
176
+ @patch("os.path.exists", return_value=False)
177
+ def test_lockdown_missing(self, mock_exists):
178
+ """Test lockdown function"""
179
+ lockdown = check_lockdown()
180
+ mock_exists.assert_called_once_with("/sys/kernel/security/lockdown")
181
+ self.assertFalse(lockdown)
182
+
183
+ @patch("builtins.print")
184
+ def test_print_color(self, mocked_print):
185
+ """Test print_color function for all expected levels"""
186
+ message = "foo"
187
+ # test all color groups
188
+ for group, color in color_dict.items():
189
+ prefix = f"{group} "
190
+ print_color(message, group)
191
+ mocked_print.assert_called_once_with(
192
+ f"{prefix}{color}{message}{Colors.ENDC}"
193
+ )
194
+ mocked_print.reset_mock()
195
+
196
+ # call without a group
197
+ print_color(message, Colors.WARNING)
198
+ mocked_print.assert_called_once_with(f"{Colors.WARNING}{message}{Colors.ENDC}")
199
+ mocked_print.reset_mock()
200
+
201
+ # test dumb terminal
202
+ os.environ["TERM"] = "dumb"
203
+ print_color(message, Colors.WARNING)
204
+ mocked_print.assert_called_once_with(f"{message}")
205
+
206
+ @patch("builtins.print")
207
+ def test_fatal_error(self, mocked_print):
208
+ """Test fatal_error function"""
209
+ with patch("sys.exit") as mock_exit:
210
+ fatal_error("foo")
211
+ mocked_print.assert_called_once_with(f"👀 {Colors.FAIL}foo{Colors.ENDC}")
212
+ mock_exit.assert_called_once_with(1)
213
+
214
+ @patch("os.geteuid", return_value=0)
215
+ def test_is_root_true(self, mock_geteuid):
216
+ """Test is_root function when user is root"""
217
+ self.assertTrue(is_root())
218
+ mock_geteuid.assert_called_once()
219
+ self.assertEqual(mock_geteuid.call_count, 1)
220
+
221
+ @patch("os.geteuid", return_value=1000)
222
+ def test_is_root_false(self, mock_geteuid):
223
+ """Test is_root function when user is not root"""
224
+ self.assertFalse(is_root())
225
+ mock_geteuid.assert_called_once()
226
+ self.assertEqual(mock_geteuid.call_count, 1)
227
+
228
+ def test_get_log_priority(self):
229
+ """Test get_log_priority works for expected values"""
230
+ ret = get_log_priority(None)
231
+ self.assertEqual(ret, "○")
232
+ ret = get_log_priority("foo")
233
+ self.assertEqual(ret, "foo")
234
+ ret = get_log_priority("3")
235
+ self.assertEqual(ret, "❌")
236
+ ret = get_log_priority(4)
237
+ self.assertEqual(ret, "🚦")
238
+ ret = get_log_priority(7)
239
+ self.assertEqual(ret, "🦟")
240
+
241
+ def test_minimum_kernel(self):
242
+ """Test minimum_kernel function"""
243
+ with patch("platform.uname") as mock_uname:
244
+ mock_uname.return_value = uname_result(
245
+ system="Linux",
246
+ node="foo",
247
+ release="6.12.0-rc5",
248
+ version="baz",
249
+ machine="x86_64",
250
+ )
251
+ self.assertTrue(minimum_kernel("6", "12"))
252
+ self.assertFalse(minimum_kernel("6", "13"))
253
+ self.assertTrue(minimum_kernel(5, 1))
254
+ self.assertFalse(minimum_kernel(7, 1))
255
+ with self.assertRaises(ValueError):
256
+ minimum_kernel("foo", "bar")
257
+ with self.assertRaises(TypeError):
258
+ minimum_kernel(None, None)
259
+
260
+ def test_systemd_in_use(self):
261
+ """Test systemd_in_use function"""
262
+ with patch(
263
+ "builtins.open", new_callable=mock_open, read_data="systemd"
264
+ ) as mock_file:
265
+ self.assertTrue(systemd_in_use())
266
+ mock_file.assert_called_once_with("/proc/1/comm", "r", encoding="utf-8")
267
+ with patch(
268
+ "builtins.open", new_callable=mock_open, read_data="upstart"
269
+ ) as mock_file:
270
+ self.assertFalse(systemd_in_use())
271
+ mock_file.assert_called_once_with("/proc/1/comm", "r", encoding="utf-8")
272
+
273
+ def test_running_in_ssh(self):
274
+ """Test running_in_ssh function"""
275
+ with patch("os.environ", {"SSH_TTY": "/dev/pts/0"}):
276
+ self.assertTrue(running_ssh())
277
+ with patch("os.environ", {}):
278
+ self.assertFalse(running_ssh())
279
+
280
+ def test_apply_prefix_wrapper(self):
281
+ """Test apply_prefix_wrapper function"""
282
+ header = "Header:"
283
+ message = "Line 1\nLine 2\nLine 3"
284
+ expected_output = "Header:\n" "│ Line 1\n" "│ Line 2\n" "└─ Line 3\n"
285
+ self.assertEqual(apply_prefix_wrapper(header, message), expected_output)
286
+
287
+ # Test with a single line message
288
+ message = "Single Line"
289
+ expected_output = "Header:\n└─ Single Line\n"
290
+ self.assertEqual(apply_prefix_wrapper(header, message), expected_output)
291
+
292
+ # Test with an empty message
293
+ message = ""
294
+ expected_output = "Header:\n"
295
+ self.assertEqual(apply_prefix_wrapper(header, message), expected_output)
296
+
297
+ # Test with leading/trailing whitespace in the message
298
+ message = " Line 1\nLine 2 \n Line 3 "
299
+ expected_output = "Header:\n" "│ Line 1\n" "│ Line 2\n" "└─ Line 3\n"
300
+ self.assertEqual(apply_prefix_wrapper(header, message), expected_output)
301
+
302
+ def test_colorize_choices_with_default(self):
303
+ """Test colorize_choices function with a default value"""
304
+ choices = ["option1", "option2", "option3"]
305
+ default = "option2"
306
+ expected_output = f"{Colors.OK}{default}{Colors.ENDC}, option1, option3"
307
+ self.assertEqual(colorize_choices(choices, default), expected_output)
308
+
309
+ def test_colorize_choices_without_default(self):
310
+ """Test colorize_choices function when default is not in choices"""
311
+ choices = ["option1", "option2", "option3"]
312
+ default = "option4"
313
+ with self.assertRaises(ValueError) as context:
314
+ colorize_choices(choices, default)
315
+ self.assertEqual(
316
+ str(context.exception), "Default choice 'option4' not in choices"
317
+ )
318
+
319
+ def test_colorize_choices_empty_list(self):
320
+ """Test colorize_choices function with an empty list"""
321
+ choices = []
322
+ default = "option1"
323
+ with self.assertRaises(ValueError) as context:
324
+ colorize_choices(choices, default)
325
+ self.assertEqual(
326
+ str(context.exception), "Default choice 'option1' not in choices"
327
+ )
328
+
329
+ def test_colorize_choices_single_choice(self):
330
+ """Test colorize_choices function with a single choice"""
331
+ choices = ["option1"]
332
+ default = "option1"
333
+ expected_output = f"{Colors.OK}{default}{Colors.ENDC}"
334
+ self.assertEqual(colorize_choices(choices, default), expected_output)
335
+
336
+ @patch("amd_debug.common.read_file")
337
+ @patch("os.path.exists")
338
+ def test_find_ip_version_found(self, mock_exists, mock_read_file):
339
+ """Test find_ip_version returns True when expected value is found"""
340
+ base_path = "/foo"
341
+ kind = "bar"
342
+ hw_ver = {"baz": 42}
343
+
344
+ # Simulate file exists and value matches
345
+ def exists_side_effect(path):
346
+ return True
347
+
348
+ mock_exists.side_effect = exists_side_effect
349
+ mock_read_file.return_value = "42"
350
+ result = find_ip_version(base_path, kind, hw_ver)
351
+ self.assertTrue(result)
352
+ b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0")
353
+ expected_path = os.path.join(b, "baz")
354
+ mock_exists.assert_any_call(expected_path)
355
+ mock_read_file.assert_any_call(expected_path)
356
+
357
+ @patch("amd_debug.common.read_file")
358
+ @patch("os.path.exists")
359
+ def test_find_ip_version_not_found_due_to_missing_file(
360
+ self, mock_exists, mock_read_file
361
+ ):
362
+ """Test find_ip_version returns False if file does not exist"""
363
+ base_path = "/foo"
364
+ kind = "bar"
365
+ hw_ver = {"baz": 42}
366
+ # Simulate file does not exist
367
+ mock_exists.return_value = False
368
+ result = find_ip_version(base_path, kind, hw_ver)
369
+ self.assertFalse(result)
370
+ b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0")
371
+ expected_path = os.path.join(b, "baz")
372
+ mock_exists.assert_any_call(expected_path)
373
+ mock_read_file.assert_not_called()
374
+
375
+ @patch("amd_debug.common.read_file")
376
+ @patch("os.path.exists")
377
+ def test_find_ip_version_not_found_due_to_value_mismatch(
378
+ self, mock_exists, mock_read_file
379
+ ):
380
+ """Test find_ip_version returns False if value does not match"""
381
+ base_path = "/foo"
382
+ kind = "bar"
383
+ hw_ver = {"baz": 42}
384
+ # Simulate file exists but value does not match
385
+ mock_exists.return_value = True
386
+ mock_read_file.return_value = "99"
387
+ result = find_ip_version(base_path, kind, hw_ver)
388
+ self.assertFalse(result)
389
+ b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0")
390
+ expected_path = os.path.join(b, "baz")
391
+ mock_exists.assert_any_call(expected_path)
392
+ mock_read_file.assert_any_call(expected_path)
393
+
394
+ @patch("amd_debug.common.read_file")
395
+ @patch("os.path.exists")
396
+ def test_find_ip_version_multiple_keys(self, mock_exists, mock_read_file):
397
+ """Test find_ip_version with multiple keys in hw_ver"""
398
+ base_path = "/foo"
399
+ kind = "bar"
400
+ hw_ver = {"baz": 42, "qux": 99}
401
+
402
+ # First key: file exists, value does not match
403
+ # Second key: file exists, value matches
404
+ def exists_side_effect(path):
405
+ return True
406
+
407
+ def read_file_side_effect(path):
408
+ if path.endswith("baz"):
409
+ return "0"
410
+ if path.endswith("qux"):
411
+ return "99"
412
+ return "0"
413
+
414
+ mock_exists.side_effect = exists_side_effect
415
+ mock_read_file.side_effect = read_file_side_effect
416
+ result = find_ip_version(base_path, kind, hw_ver)
417
+ self.assertFalse(result)
418
+
419
+ def test_convert_string_to_bool_true_values(self):
420
+ """Test convert_string_to_bool returns True for truthy string values"""
421
+ self.assertTrue(convert_string_to_bool("True"))
422
+ self.assertTrue(convert_string_to_bool("1"))
423
+ self.assertTrue(convert_string_to_bool("'nonempty'"))
424
+ self.assertTrue(convert_string_to_bool('"nonempty"'))
425
+
426
+ def test_convert_string_to_bool_false_values(self):
427
+ """Test convert_string_to_bool returns False for falsy string values"""
428
+ self.assertFalse(convert_string_to_bool("False"))
429
+ self.assertFalse(convert_string_to_bool("0"))
430
+ self.assertFalse(convert_string_to_bool("''"))
431
+ self.assertFalse(convert_string_to_bool('""'))
432
+ self.assertFalse(convert_string_to_bool("None"))
433
+
434
+ def test_convert_string_to_bool_invalid_syntax(self):
435
+ """Test convert_string_to_bool exits on invalid syntax"""
436
+ with patch("sys.exit") as mock_exit:
437
+ convert_string_to_bool("not_a_bool")
438
+ mock_exit.assert_called_once_with("Invalid entry: not_a_bool")
439
+
440
+ def test_convert_string_to_bool_invalid_value(self):
441
+ """Test convert_string_to_bool exits on invalid value"""
442
+ with patch("sys.exit") as mock_exit:
443
+ convert_string_to_bool("[unclosed_list")
444
+ mock_exit.assert_called_once_with("Invalid entry: [unclosed_list")