amd-debug-tools 0.2.4__py3-none-any.whl → 0.2.6__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.

Potentially problematic release.


This version of amd-debug-tools might be problematic. Click here for more details.

amd_debug/__init__.py CHANGED
@@ -23,8 +23,15 @@ def amd_pstate():
23
23
  return pstate.main()
24
24
 
25
25
 
26
+ def amd_ttm():
27
+ """Launch the amd-ttm tool."""
28
+ from . import ttm # pylint: disable=import-outside-toplevel
29
+
30
+ return ttm.main()
31
+
32
+
26
33
  def install_dep_superset():
27
- """Install all supserset dependencies."""
34
+ """Install all superset dependencies."""
28
35
  from . import installer # pylint: disable=import-outside-toplevel
29
36
 
30
37
  return installer.install_dep_superset()
@@ -36,6 +43,7 @@ def launch_tool(tool_name):
36
43
  "amd_s2idle.py": amd_s2idle,
37
44
  "amd_bios.py": amd_bios,
38
45
  "amd_pstate.py": amd_pstate,
46
+ "amd_ttm.py": amd_ttm,
39
47
  "install_deps.py": install_dep_superset,
40
48
  }
41
49
  if tool_name in tools:
amd_debug/bios.py CHANGED
@@ -105,7 +105,9 @@ def parse_args():
105
105
  action="store_true",
106
106
  help="Enable tool debug logging",
107
107
  )
108
- subparsers.add_parser("version", help="Show version information")
108
+ parser.add_argument(
109
+ "--version", action="store_true", help="Show version information"
110
+ )
109
111
 
110
112
  if len(sys.argv) == 1:
111
113
  parser.print_help(sys.stderr)
@@ -122,7 +124,7 @@ def parse_args():
122
124
  return args
123
125
 
124
126
 
125
- def main() -> None|int:
127
+ def main() -> None | int:
126
128
  """Main function"""
127
129
  args = parse_args()
128
130
  ret = False
@@ -132,7 +134,7 @@ def main() -> None|int:
132
134
  elif args.command == "parse":
133
135
  app = AmdBios(args.input, args.tool_debug)
134
136
  ret = app.run()
135
- elif args.command == "version":
137
+ elif args.version:
136
138
  print(version())
137
139
  show_log_info()
138
140
  if ret is False:
amd_debug/common.py CHANGED
@@ -226,6 +226,45 @@ def get_pretty_distro() -> str:
226
226
  return distro
227
227
 
228
228
 
229
+ def bytes_to_gb(bytes_value):
230
+ """Convert bytes to GB"""
231
+ return bytes_value * 4096 / (1024 * 1024 * 1024)
232
+
233
+
234
+ def gb_to_pages(gb_value):
235
+ """Convert GB into bytes"""
236
+ return int(gb_value * (1024 * 1024 * 1024) / 4096)
237
+
238
+
239
+ def reboot():
240
+ """Reboot the system"""
241
+ try:
242
+ import dbus # pylint: disable=import-outside-toplevel
243
+
244
+ bus = dbus.SystemBus()
245
+ obj = bus.get_object("org.freedesktop.login1", "/org/freedesktop/login1")
246
+ intf = dbus.Interface(obj, "org.freedesktop.login1.Manager")
247
+ intf.Reboot(True)
248
+ return True
249
+ except ImportError:
250
+ fatal_error("Missing dbus")
251
+ except dbus.exceptions.DBusException as e:
252
+ fatal_error({e})
253
+ return True
254
+
255
+
256
+ def get_system_mem():
257
+ """Get the total system memory in GB using /proc/meminfo"""
258
+ with open(os.path.join("/", "proc", "meminfo"), "r", encoding="utf-8") as f:
259
+ for line in f:
260
+ if line.startswith("MemTotal:"):
261
+ # MemTotal line format: "MemTotal: 16384516 kB"
262
+ # Extract the number and convert from kB to GB
263
+ mem_kb = int(line.split()[1])
264
+ return mem_kb / (1024 * 1024)
265
+ raise ValueError("Could not find MemTotal in /proc/meminfo")
266
+
267
+
229
268
  def is_root() -> bool:
230
269
  """Check if the user is root"""
231
270
  return os.geteuid() == 0
amd_debug/installer.py CHANGED
@@ -446,8 +446,8 @@ def parse_args():
446
446
  return parser.parse_args()
447
447
 
448
448
 
449
- def install_dep_superset() -> None|int:
450
- """Install all python supserset dependencies"""
449
+ def install_dep_superset() -> None | int:
450
+ """Install all python superset dependencies"""
451
451
  args = parse_args()
452
452
  tool = Installer(tool_debug=args.tool_debug)
453
453
  tool.set_requirements(
amd_debug/kernel.py CHANGED
@@ -115,24 +115,16 @@ def sscanf_bios_args(line):
115
115
 
116
116
  converted_args = []
117
117
  arg_index = 0
118
- for specifier in format_specifiers:
118
+ for _specifier in format_specifiers:
119
119
  if arg_index < len(arguments):
120
120
  value = arguments[arg_index]
121
121
  if value == "Unknown":
122
122
  converted_args.append(-1)
123
- elif specifier.lower() == "x":
123
+ else:
124
124
  try:
125
125
  converted_args.append(int(value, 16))
126
126
  except ValueError:
127
127
  return None
128
- else: # Decimal conversion
129
- try:
130
- converted_args.append(int(value))
131
- except ValueError:
132
- try:
133
- converted_args.append(int(value, 16))
134
- except ValueError:
135
- return None
136
128
  arg_index += 1
137
129
  else:
138
130
  break
@@ -128,6 +128,28 @@ class PrerequisiteValidator(AmdTool):
128
128
  if not self.db.get_last_prereq_ts():
129
129
  self.run()
130
130
 
131
+ def capture_nvidia(self):
132
+ """Capture the NVIDIA GPU state"""
133
+ p = os.path.join("/", "proc", "driver", "nvidia", "version")
134
+ if not os.path.exists(p):
135
+ return True
136
+ try:
137
+ self.db.record_debug_file(p)
138
+ except PermissionError:
139
+ self.db.record_prereq("NVIDIA GPU version not readable", "👀")
140
+ return True
141
+ p = os.path.join("/", "proc", "driver", "nvidia", "gpus")
142
+ if not os.path.exists(p):
143
+ return True
144
+ for root, _dirs, files in os.walk(p, topdown=False):
145
+ for f in files:
146
+ try:
147
+ self.db.record_debug(f"NVIDIA {f}")
148
+ self.db.record_debug_file(os.path.join(root, f))
149
+ except PermissionError:
150
+ self.db.record_prereq("NVIDIA GPU {f} not readable", "👀")
151
+ return True
152
+
131
153
  def capture_edid(self):
132
154
  """Capture and decode the EDID data"""
133
155
  edids = self.display.get_edid()
@@ -1145,26 +1167,13 @@ class PrerequisiteValidator(AmdTool):
1145
1167
  found_iommu = True
1146
1168
  debug_str += f"Found IOMMU {dev.sys_path}\n"
1147
1169
  break
1170
+
1171
+ # User turned off IOMMU, no problems
1148
1172
  if not found_iommu:
1149
1173
  self.db.record_prereq("IOMMU disabled", "✅")
1150
1174
  return True
1151
- p = os.path.join("/", "sys", "firmware", "acpi", "tables", "IVRS")
1152
- with open(p, "rb") as f:
1153
- data = f.read()
1154
- if len(data) < 40:
1155
- raise ValueError(
1156
- "IVRS table appears too small to contain virtualization info."
1157
- )
1158
- virt_info = struct.unpack_from("I", data, 36)[0]
1159
- debug_str += f"Virtualization info: 0x{virt_info:x}"
1160
- found_dmar = (virt_info & 0x2) != 0
1161
- self.db.record_debug(debug_str)
1162
- if not found_dmar:
1163
- self.db.record_prereq(
1164
- "IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌"
1165
- )
1166
- self.failures += [DMArNotEnabled()]
1167
- return False
1175
+
1176
+ # Look for MSFT0201 in DSDT/SSDT
1168
1177
  for dev in self.pyudev.list_devices(subsystem="acpi"):
1169
1178
  if "MSFT0201" in dev.sys_path:
1170
1179
  found_acpi = True
@@ -1174,13 +1183,39 @@ class PrerequisiteValidator(AmdTool):
1174
1183
  )
1175
1184
  self.failures += [MissingIommuACPI("MSFT0201")]
1176
1185
  return False
1177
- # check that policy is bound to it
1186
+
1187
+ # Check that policy is bound to it
1178
1188
  for dev in self.pyudev.list_devices(subsystem="platform"):
1179
1189
  if "MSFT0201" in dev.sys_path:
1180
1190
  p = os.path.join(dev.sys_path, "iommu")
1181
1191
  if not os.path.exists(p):
1182
1192
  self.failures += [MissingIommuPolicy("MSFT0201")]
1183
1193
  return False
1194
+
1195
+ # Check pre-boot DMA
1196
+ p = os.path.join("/", "sys", "firmware", "acpi", "tables", "IVRS")
1197
+ with open(p, "rb") as f:
1198
+ data = f.read()
1199
+ if len(data) < 40:
1200
+ raise ValueError(
1201
+ "IVRS table appears too small to contain virtualization info."
1202
+ )
1203
+ virt_info = struct.unpack_from("I", data, 36)[0]
1204
+ debug_str += f"IVRS: Virtualization info: 0x{virt_info:x}\n"
1205
+ found_ivrs_dmar = (virt_info & 0x2) != 0
1206
+
1207
+ # check for MSFT0201 in IVRS (alternative to pre-boot DMA)
1208
+ target_bytes = "MSFT0201".encode("utf-8")
1209
+ found_ivrs_msft0201 = data.find(target_bytes) != -1
1210
+ debug_str += f"IVRS: Found MSFT0201: {found_ivrs_msft0201}"
1211
+
1212
+ self.db.record_debug(debug_str)
1213
+ if not found_ivrs_dmar and not found_ivrs_msft0201:
1214
+ self.db.record_prereq(
1215
+ "IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌"
1216
+ )
1217
+ self.failures += [DMArNotEnabled()]
1218
+ return False
1184
1219
  self.db.record_prereq("IOMMU properly configured", "✅")
1185
1220
  return True
1186
1221
 
@@ -1190,6 +1225,8 @@ class PrerequisiteValidator(AmdTool):
1190
1225
  return True
1191
1226
  if self.cpu_model not in [0x74, 0x78]:
1192
1227
  return True
1228
+ if not self.smu_version:
1229
+ return True
1193
1230
  if version.parse(self.smu_version) > version.parse("76.60.0"):
1194
1231
  return True
1195
1232
  if version.parse(self.smu_version) < version.parse("76.18.0"):
@@ -1212,9 +1249,8 @@ class PrerequisiteValidator(AmdTool):
1212
1249
  # ignore kernel warnings
1213
1250
  taint &= ~BIT(9)
1214
1251
  if taint != 0:
1215
- self.db.record_prereq(f"Kernel is tainted: {taint}", "")
1252
+ self.db.record_prereq(f"Kernel is tainted: {taint}", "🚦")
1216
1253
  self.failures += [TaintedKernel()]
1217
- return False
1218
1254
  return True
1219
1255
 
1220
1256
  def run(self):
@@ -1229,6 +1265,7 @@ class PrerequisiteValidator(AmdTool):
1229
1265
  self.capture_logind,
1230
1266
  self.capture_pci_acpi,
1231
1267
  self.capture_edid,
1268
+ self.capture_nvidia,
1232
1269
  ]
1233
1270
  checks = []
1234
1271
 
amd_debug/pstate.py CHANGED
@@ -293,18 +293,20 @@ def parse_args():
293
293
  action="store_true",
294
294
  help="Enable tool debug logging",
295
295
  )
296
- subparsers.add_parser("version", help="Show version information")
296
+ parser.add_argument(
297
+ "--version", action="store_true", help="Show version information"
298
+ )
297
299
  if len(sys.argv) == 1:
298
300
  parser.print_help(sys.stderr)
299
301
  sys.exit(1)
300
302
  return parser.parse_args()
301
303
 
302
304
 
303
- def main() -> None|int:
305
+ def main() -> None | int:
304
306
  """Main function"""
305
307
  args = parse_args()
306
308
  ret = False
307
- if args.command == "version":
309
+ if args.version:
308
310
  print(version())
309
311
  return
310
312
  elif args.command == "triage":
amd_debug/s2idle.py CHANGED
@@ -385,7 +385,9 @@ def parse_args():
385
385
  help="Enable tool debug logging",
386
386
  )
387
387
 
388
- subparsers.add_parser("version", help="Show version information")
388
+ parser.add_argument(
389
+ "--version", action="store_true", help="Show version information"
390
+ )
389
391
 
390
392
  if len(sys.argv) == 1:
391
393
  parser.print_help(sys.stderr)
@@ -394,7 +396,7 @@ def parse_args():
394
396
  return parser.parse_args()
395
397
 
396
398
 
397
- def main() -> None|int:
399
+ def main() -> None | int:
398
400
  """Main function"""
399
401
  args = parse_args()
400
402
  ret = False
@@ -427,7 +429,7 @@ def main() -> None|int:
427
429
  args.logind,
428
430
  args.bios_debug,
429
431
  )
430
- elif args.action == "version":
432
+ elif args.version:
431
433
  print(version())
432
434
  return
433
435
  else:
amd_debug/test_ttm.py ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ This module contains unit tests for the ttm tool in the amd-debug-tools package.
6
+ """
7
+ import unittest
8
+ import sys
9
+ import logging
10
+ from unittest import mock
11
+
12
+ from amd_debug.ttm import main, parse_args, AmdTtmTool, maybe_reboot
13
+
14
+
15
+ class TestParseArgs(unittest.TestCase):
16
+ """Test parse_args function"""
17
+
18
+ @classmethod
19
+ def setUpClass(cls):
20
+ logging.basicConfig(filename="/dev/null", level=logging.DEBUG)
21
+
22
+ def setUp(self):
23
+ self.default_sys_argv = sys.argv
24
+
25
+ def tearDown(self):
26
+ sys.argv = self.default_sys_argv
27
+
28
+ @mock.patch.object(sys, "argv", new=["ttm", "--version"])
29
+ def test_parse_args_version(self):
30
+ """Test version argument"""
31
+ args = parse_args()
32
+ self.assertTrue(args.version)
33
+ self.assertFalse(args.set)
34
+ self.assertFalse(args.clear)
35
+
36
+ @mock.patch.object(sys, "argv", new=["ttm", "--clear"])
37
+ def test_parse_args_clear(self):
38
+ """Test clear argument"""
39
+ args = parse_args()
40
+ self.assertFalse(args.version)
41
+ self.assertFalse(args.set)
42
+ self.assertTrue(args.clear)
43
+
44
+
45
+ class TestMainFunction(unittest.TestCase):
46
+ """Test main() function logic"""
47
+
48
+ @mock.patch("amd_debug.ttm.parse_args")
49
+ @mock.patch("amd_debug.ttm.version", return_value="1.2.3")
50
+ @mock.patch("builtins.print")
51
+ def test_main_version(self, mock_print, _mock_version, mock_parse_args):
52
+ """Test main function with version argument"""
53
+ mock_parse_args.return_value = mock.Mock(
54
+ version=True, set=None, clear=False, tool_debug=False
55
+ )
56
+ ret = main()
57
+ mock_print.assert_called_with("1.2.3")
58
+ self.assertIsNone(ret)
59
+
60
+ @mock.patch("amd_debug.ttm.parse_args")
61
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
62
+ @mock.patch("builtins.print")
63
+ def test_main_set_invalid(self, mock_print, _mock_tool, mock_parse_args):
64
+ """Test main function with invalid set argument"""
65
+ mock_parse_args.return_value = mock.Mock(
66
+ version=False, set=0, clear=False, tool_debug=False
67
+ )
68
+ ret = main()
69
+ mock_print.assert_called_with("Error: GB value must be greater than 0")
70
+ self.assertEqual(ret, 1)
71
+
72
+ @mock.patch("amd_debug.ttm.parse_args")
73
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
74
+ def test_main_set_valid(self, mock_tool, mock_parse_args):
75
+ """Test main function with set argument"""
76
+ instance = mock_tool.return_value
77
+ instance.set.return_value = True
78
+ mock_parse_args.return_value = mock.Mock(
79
+ version=False, set=2, clear=False, tool_debug=False
80
+ )
81
+ ret = main()
82
+ instance.set.assert_called_with(2)
83
+ self.assertIsNone(ret)
84
+
85
+ @mock.patch("amd_debug.ttm.parse_args")
86
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
87
+ def test_main_set_failed(self, mock_tool, mock_parse_args):
88
+ instance = mock_tool.return_value
89
+ instance.set.return_value = False
90
+ mock_parse_args.return_value = mock.Mock(
91
+ version=False, set=2, clear=False, tool_debug=False
92
+ )
93
+ ret = main()
94
+ instance.set.assert_called_with(2)
95
+ self.assertEqual(ret, 1)
96
+
97
+ @mock.patch("amd_debug.ttm.parse_args")
98
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
99
+ def test_main_clear_success(self, mock_tool, mock_parse_args):
100
+ """Test main function with clear argument"""
101
+ instance = mock_tool.return_value
102
+ instance.clear.return_value = True
103
+ mock_parse_args.return_value = mock.Mock(
104
+ version=False, set=None, clear=True, tool_debug=False
105
+ )
106
+ ret = main()
107
+ instance.clear.assert_called_once()
108
+ self.assertIsNone(ret)
109
+
110
+ @mock.patch("amd_debug.ttm.parse_args")
111
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
112
+ def test_main_clear_failed(self, mock_tool, mock_parse_args):
113
+ """Test main function with clear argument failure"""
114
+ instance = mock_tool.return_value
115
+ instance.clear.return_value = False
116
+ mock_parse_args.return_value = mock.Mock(
117
+ version=False, set=None, clear=True, tool_debug=False
118
+ )
119
+ ret = main()
120
+ instance.clear.assert_called_once()
121
+ self.assertEqual(ret, 1)
122
+
123
+ @mock.patch("amd_debug.ttm.parse_args")
124
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
125
+ def test_main_get_success(self, mock_tool, mock_parse_args):
126
+ """Test main function with get argument"""
127
+ instance = mock_tool.return_value
128
+ instance.get.return_value = True
129
+ mock_parse_args.return_value = mock.Mock(
130
+ version=False, set=None, clear=False, tool_debug=False
131
+ )
132
+ ret = main()
133
+ instance.get.assert_called_once()
134
+ self.assertIsNone(ret)
135
+
136
+ @mock.patch("amd_debug.ttm.parse_args")
137
+ @mock.patch("amd_debug.ttm.AmdTtmTool")
138
+ def test_main_get_failed(self, mock_tool, mock_parse_args):
139
+ """Test main function with get argument failure"""
140
+ instance = mock_tool.return_value
141
+ instance.get.return_value = False
142
+ mock_parse_args.return_value = mock.Mock(
143
+ version=False, set=None, clear=False, tool_debug=False
144
+ )
145
+ ret = main()
146
+ instance.get.assert_called_once()
147
+ self.assertEqual(ret, 1)
148
+
149
+
150
+ class TestMaybeReboot(unittest.TestCase):
151
+ """Test maybe_reboot function"""
152
+
153
+ @mock.patch("builtins.input", return_value="y")
154
+ @mock.patch("amd_debug.ttm.reboot", return_value=True)
155
+ def test_maybe_reboot_yes(self, mock_reboot, _mock_input):
156
+ """Test reboot confirmation and execution"""
157
+ result = maybe_reboot()
158
+ mock_reboot.assert_called_once()
159
+ self.assertTrue(result)
160
+
161
+ @mock.patch("builtins.input", return_value="n")
162
+ @mock.patch("amd_debug.ttm.reboot", return_value=True)
163
+ def test_maybe_reboot_no(self, mock_reboot, _mock_input):
164
+ """Test reboot confirmation without execution"""
165
+ result = maybe_reboot()
166
+ mock_reboot.assert_not_called()
167
+ self.assertTrue(result)
168
+
169
+
170
+ class TestAmdTtmTool(unittest.TestCase):
171
+ """Unit tests for AmdTtmTool class"""
172
+
173
+ def setUp(self):
174
+ self.tool = AmdTtmTool(logging=False)
175
+
176
+ @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="4096")
177
+ @mock.patch("amd_debug.ttm.bytes_to_gb", return_value=4.0)
178
+ @mock.patch("amd_debug.ttm.print_color")
179
+ @mock.patch("amd_debug.ttm.get_system_mem", return_value=16.0)
180
+ def test_get_success(self, _mock_mem, mock_print, _mock_bytes_to_gb, mock_open):
181
+ """Test get() when TTM_PARAM_PATH exists"""
182
+ result = self.tool.get()
183
+ mock_open.assert_called_once_with(
184
+ "/sys/module/ttm/parameters/pages_limit", "r", encoding="utf-8"
185
+ )
186
+ mock_print.assert_any_call(
187
+ "Current TTM pages limit: 4096 pages (4.00 GB)", "💻"
188
+ )
189
+ mock_print.assert_any_call("Total system memory: 16.00 GB", "💻")
190
+ self.assertTrue(result)
191
+
192
+ @mock.patch("builtins.open", side_effect=FileNotFoundError)
193
+ @mock.patch("amd_debug.ttm.print_color")
194
+ def test_get_file_not_found(self, mock_print, _mock_open):
195
+ """Test get() when TTM_PARAM_PATH does not exist"""
196
+ result = self.tool.get()
197
+ mock_print.assert_called_with(
198
+ "Error: Could not find /sys/module/ttm/parameters/pages_limit", "❌"
199
+ )
200
+ self.assertFalse(result)
201
+
202
+ @mock.patch("amd_debug.ttm.is_root", return_value=False)
203
+ @mock.patch("amd_debug.ttm.print_color")
204
+ def test_set_not_root(self, mock_print, _mock_is_root):
205
+ """Test set() when not root"""
206
+ result = self.tool.set(2)
207
+ mock_print.assert_called_with("Root privileges required", "❌")
208
+ self.assertFalse(result)
209
+
210
+ @mock.patch("amd_debug.ttm.is_root", return_value=True)
211
+ @mock.patch("amd_debug.ttm.get_system_mem", return_value=8.0)
212
+ @mock.patch("amd_debug.ttm.print_color")
213
+ def test_set_gb_greater_than_total(self, mock_print, _mock_mem, _mock_is_root):
214
+ """Test set() when gb_value > total system memory"""
215
+ result = self.tool.set(16)
216
+ mock_print.assert_any_call(
217
+ "16.00 GB is greater than total system memory (8.00 GB)", "❌"
218
+ )
219
+ self.assertFalse(result)
220
+
221
+ @mock.patch("amd_debug.ttm.is_root", return_value=True)
222
+ @mock.patch("amd_debug.ttm.get_system_mem", return_value=10.0)
223
+ @mock.patch("amd_debug.ttm.print_color")
224
+ @mock.patch("builtins.input", return_value="n")
225
+ def test_set_gb_exceeds_max_percentage_cancel(
226
+ self, _mock_input, mock_print, _mock_mem, mock_is_root
227
+ ):
228
+ """Test set() when gb_value exceeds max percentage and user cancels"""
229
+ result = self.tool.set(9.5)
230
+ self.assertFalse(result)
231
+ mock_print.assert_any_call("Operation cancelled.", "🚦")
232
+
233
+ @mock.patch("amd_debug.ttm.is_root", return_value=True)
234
+ @mock.patch("amd_debug.ttm.get_system_mem", return_value=10.0)
235
+ @mock.patch("amd_debug.ttm.gb_to_pages", return_value=20480)
236
+ @mock.patch("amd_debug.ttm.print_color")
237
+ @mock.patch("builtins.open", new_callable=mock.mock_open)
238
+ @mock.patch("builtins.input", return_value="y")
239
+ @mock.patch("amd_debug.ttm.maybe_reboot", return_value=True)
240
+ def test_set_success(
241
+ self,
242
+ _mock_reboot,
243
+ _mock_input,
244
+ mock_open,
245
+ mock_print,
246
+ _mock_gb_to_pages,
247
+ mock_mem,
248
+ mock_is_root,
249
+ ):
250
+ """Test set() success path"""
251
+ result = self.tool.set(5)
252
+ mock_open.assert_called_once_with(
253
+ "/etc/modprobe.d/ttm.conf", "w", encoding="utf-8"
254
+ )
255
+ mock_print.assert_any_call(
256
+ "Successfully set TTM pages limit to 20480 pages (5.00 GB)", "🐧"
257
+ )
258
+ self.assertTrue(result)
259
+
260
+ @mock.patch("os.path.exists", return_value=False)
261
+ @mock.patch("amd_debug.ttm.print_color")
262
+ def test_clear_file_not_exists(self, mock_print, _mock_exists):
263
+ """Test clear() when config file does not exist"""
264
+ result = self.tool.clear()
265
+ mock_print.assert_called_with("/etc/modprobe.d/ttm.conf doesn't exist", "❌")
266
+ self.assertFalse(result)
267
+
268
+ @mock.patch("os.path.exists", return_value=True)
269
+ @mock.patch("amd_debug.ttm.is_root", return_value=False)
270
+ @mock.patch("amd_debug.ttm.print_color")
271
+ def test_clear_not_root(self, mock_print, _mock_is_root, _mock_exists):
272
+ """Test clear() when not root"""
273
+ result = self.tool.clear()
274
+ mock_print.assert_called_with("Root privileges required", "❌")
275
+ self.assertFalse(result)
276
+
277
+ @mock.patch("os.path.exists", return_value=True)
278
+ @mock.patch("amd_debug.ttm.is_root", return_value=True)
279
+ @mock.patch("os.remove")
280
+ @mock.patch("amd_debug.ttm.print_color")
281
+ @mock.patch("amd_debug.ttm.maybe_reboot", return_value=True)
282
+ def test_clear_success(
283
+ self, _mock_reboot, mock_print, mock_remove, _mock_is_root, mock_exists
284
+ ):
285
+ """Test clear() success path"""
286
+ result = self.tool.clear()
287
+ mock_remove.assert_called_once_with("/etc/modprobe.d/ttm.conf")
288
+ mock_print.assert_any_call(
289
+ "Configuration /etc/modprobe.d/ttm.conf removed", "🐧"
290
+ )
291
+ self.assertTrue(result)