amd-debug-tools 0.2.5__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(
@@ -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()
@@ -1227,9 +1249,8 @@ class PrerequisiteValidator(AmdTool):
1227
1249
  # ignore kernel warnings
1228
1250
  taint &= ~BIT(9)
1229
1251
  if taint != 0:
1230
- self.db.record_prereq(f"Kernel is tainted: {taint}", "")
1252
+ self.db.record_prereq(f"Kernel is tainted: {taint}", "🚦")
1231
1253
  self.failures += [TaintedKernel()]
1232
- return False
1233
1254
  return True
1234
1255
 
1235
1256
  def run(self):
@@ -1244,6 +1265,7 @@ class PrerequisiteValidator(AmdTool):
1244
1265
  self.capture_logind,
1245
1266
  self.capture_pci_acpi,
1246
1267
  self.capture_edid,
1268
+ self.capture_nvidia,
1247
1269
  ]
1248
1270
  checks = []
1249
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)
amd_debug/ttm.py ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+ """TTM configuration tool"""
4
+
5
+ import os
6
+ import argparse
7
+ from amd_debug.common import (
8
+ AmdTool,
9
+ bytes_to_gb,
10
+ gb_to_pages,
11
+ get_system_mem,
12
+ is_root,
13
+ print_color,
14
+ reboot,
15
+ version,
16
+ )
17
+
18
+ TTM_PARAM_PATH = "/sys/module/ttm/parameters/pages_limit"
19
+ MODPROBE_CONF_PATH = "/etc/modprobe.d/ttm.conf"
20
+ # Maximum percentage of total system memory to allow for TTM
21
+ MAX_MEMORY_PERCENTAGE = 90
22
+
23
+
24
+ def maybe_reboot() -> bool:
25
+ """Prompt to reboot system"""
26
+ response = input("Would you like to reboot the system now? (y/n): ").strip().lower()
27
+ if response in ("y", "yes"):
28
+ return reboot()
29
+ return True
30
+
31
+
32
+ class AmdTtmTool(AmdTool):
33
+ """Class for handling TTM page configuration"""
34
+
35
+ def __init__(self, logging):
36
+ log_prefix = "ttm" if logging else None
37
+ super().__init__(log_prefix)
38
+
39
+ def get(self) -> bool:
40
+ """Read current page limit"""
41
+ try:
42
+ with open(TTM_PARAM_PATH, "r", encoding="utf-8") as f:
43
+ pages = int(f.read().strip())
44
+ gb_value = bytes_to_gb(pages)
45
+ print_color(
46
+ f"Current TTM pages limit: {pages} pages ({gb_value:.2f} GB)", "💻"
47
+ )
48
+ except FileNotFoundError:
49
+ print_color(f"Error: Could not find {TTM_PARAM_PATH}", "❌")
50
+ return False
51
+
52
+ total = get_system_mem()
53
+ if total > 0:
54
+ print_color(f"Total system memory: {total:.2f} GB", "💻")
55
+
56
+ return True
57
+
58
+ def set(self, gb_value) -> bool:
59
+ """Set a new page limit"""
60
+ if not is_root():
61
+ print_color("Root privileges required", "❌")
62
+ return False
63
+
64
+ # Check against system memory
65
+ total = get_system_mem()
66
+ if total > 0:
67
+ max_recommended_gb = total * MAX_MEMORY_PERCENTAGE / 100
68
+
69
+ if gb_value > total:
70
+ print_color(
71
+ f"{gb_value:.2f} GB is greater than total system memory ({total:.2f} GB)",
72
+ "❌",
73
+ )
74
+ return False
75
+
76
+ if gb_value > max_recommended_gb:
77
+ print_color(
78
+ f"Warning: The requested value ({gb_value:.2f} GB) exceeds {MAX_MEMORY_PERCENTAGE}% of your system memory ({max_recommended_gb:.2f} GB).",
79
+ "🚦",
80
+ )
81
+ response = (
82
+ input(
83
+ "This could cause system instability. Continue anyway? (y/n): "
84
+ )
85
+ .strip()
86
+ .lower()
87
+ )
88
+ if response not in ("y", "yes"):
89
+ print_color("Operation cancelled.", "🚦")
90
+ return False
91
+
92
+ pages = gb_to_pages(gb_value)
93
+
94
+ with open(MODPROBE_CONF_PATH, "w", encoding="utf-8") as f:
95
+ f.write(f"options ttm pages_limit={pages}\n")
96
+ print_color(
97
+ f"Successfully set TTM pages limit to {pages} pages ({gb_value:.2f} GB)",
98
+ "🐧",
99
+ )
100
+ print_color(f"Configuration written to {MODPROBE_CONF_PATH}", "🐧")
101
+ print_color("NOTE: You need to reboot for changes to take effect.", "○")
102
+
103
+ return maybe_reboot()
104
+
105
+ def clear(self) -> bool:
106
+ """Clears the page limit"""
107
+ if not os.path.exists(MODPROBE_CONF_PATH):
108
+ print_color(f"{MODPROBE_CONF_PATH} doesn't exist", "❌")
109
+ return False
110
+
111
+ if not is_root():
112
+ print_color("Root privileges required", "❌")
113
+ return False
114
+
115
+ os.remove(MODPROBE_CONF_PATH)
116
+ print_color(f"Configuration {MODPROBE_CONF_PATH} removed", "🐧")
117
+
118
+ return maybe_reboot()
119
+
120
+
121
+ def parse_args():
122
+ """Parse command line arguments."""
123
+ parser = argparse.ArgumentParser(description="Manage TTM pages limit")
124
+ parser.add_argument("--set", type=float, help="Set pages limit in GB")
125
+ parser.add_argument(
126
+ "--clear", action="store_true", help="Clear a previously set page limit"
127
+ )
128
+ parser.add_argument(
129
+ "--version", action="store_true", help="Show version information"
130
+ )
131
+ parser.add_argument(
132
+ "--tool-debug",
133
+ action="store_true",
134
+ help="Enable tool debug logging",
135
+ )
136
+
137
+ return parser.parse_args()
138
+
139
+
140
+ def main() -> None | int:
141
+ """Main function"""
142
+
143
+ args = parse_args()
144
+ tool = AmdTtmTool(args.tool_debug)
145
+ ret = False
146
+
147
+ if args.version:
148
+ print(version())
149
+ return
150
+ elif args.set is not None:
151
+ if args.set <= 0:
152
+ print("Error: GB value must be greater than 0")
153
+ return 1
154
+ ret = tool.set(args.set)
155
+ elif args.clear:
156
+ ret = tool.clear()
157
+ else:
158
+ ret = tool.get()
159
+ if ret is False:
160
+ return 1
161
+ return
amd_debug/validator.py CHANGED
@@ -703,6 +703,22 @@ class SleepValidator(AmdTool):
703
703
  else:
704
704
  print_color("No RTC device found, please manually wake system", "🚦")
705
705
 
706
+ def toggle_nvidia(self, value):
707
+ """Write to the NVIDIA suspend interface"""
708
+ p = os.path.join("/", "proc", "driver", "nvidia", "suspend")
709
+ if not os.path.exists(p):
710
+ return True
711
+ fd = os.open(p, os.O_WRONLY | os.O_SYNC)
712
+ try:
713
+ os.write(fd, value)
714
+ except OSError as e:
715
+ self.db.record_cycle_data(f"Failed to set {value} in NVIDIA {e}", "❌")
716
+ return False
717
+ finally:
718
+ os.close(fd)
719
+ self.db.record_debug(f"Wrote {value} to NVIDIA driver")
720
+ return True
721
+
706
722
  @pm_debugging
707
723
  def suspend_system(self):
708
724
  """Suspend the system using the dbus or sysfs interface"""
@@ -744,6 +760,8 @@ class SleepValidator(AmdTool):
744
760
  self.db.record_cycle_data("Missing dbus", "❌")
745
761
  return False
746
762
  else:
763
+ if not self.toggle_nvidia(b"suspend"):
764
+ return False
747
765
  old = get_wakeup_count()
748
766
  p = os.path.join("/", "sys", "power", "state")
749
767
  fd = os.open(p, os.O_WRONLY | os.O_SYNC)
@@ -757,6 +775,8 @@ class SleepValidator(AmdTool):
757
775
  return False
758
776
  finally:
759
777
  os.close(fd)
778
+ if not self.toggle_nvidia(b"resume"):
779
+ return False
760
780
  return True
761
781
 
762
782
  def unlock_session(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amd-debug-tools
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: debug tools for AMD systems
5
5
  Author-email: Mario Limonciello <superm1@kernel.org>
6
6
  License-Expression: MIT
@@ -131,7 +131,7 @@ The following optional arguments are supported for this command:
131
131
  If the tool is launched with an environment that can call `xdg-open`, the report
132
132
  will be opened in a browser.
133
133
 
134
- ### `amd-s2idle version`
134
+ ### `amd-s2idle --version`
135
135
  This will print the version of the tool and exit.
136
136
 
137
137
  ### Debug output
@@ -164,7 +164,7 @@ The following optional arguments are supported for this command:
164
164
  --input INPUT Optional input file to parse
165
165
  --tool-debug Enable tool debug logging
166
166
 
167
- ### `amd-bios version`
167
+ ### `amd-bios --version`
168
168
  This will print the version of the tool and exit.
169
169
 
170
170
  ## amd-pstate
@@ -172,6 +172,9 @@ This will print the version of the tool and exit.
172
172
  It will capture some state from the system as well as from the machine specific registers that
173
173
  amd-pstate uses.
174
174
 
175
+ ## amd-ttm
176
+ `amd-ttm` is a tool used for managing the TTM memory settings on AMD systems.
177
+
175
178
  ## Compatibility scripts
176
179
 
177
180
  Compatibility scripts are provided for the previous names the tools went by:
@@ -1,45 +1,47 @@
1
1
  launcher.py,sha256=M8kT9DtyZoQgZaKWDbSBu4jsS6tZF1gWko3sovNVyag,947
2
2
  test_acpi.py,sha256=wtS43Rz95h7YEEJBeFa6Mswaeo4syBZrw4hY8i0YbJY,3117
3
3
  test_batteries.py,sha256=nN5pfP5El7Whypq3HHEpW8bufdf5EWSTVGbayfNQYP4,3360
4
- test_bios.py,sha256=ZPqI5X0QpEJBNJP-i5gNZzlbOlVSpznH4uv34esSqD8,8984
5
- test_common.py,sha256=fb16Oilh5ga6VgF-UgBj6azoYzZnPrS7KpECQ3nCwlg,16335
4
+ test_bios.py,sha256=x_KLmQqGEbQhTugyWCHGXjGp2H1dCdhRz0kgw2Big8w,9276
5
+ test_common.py,sha256=EYbyObC9vIXquT3EbgQ_98V4Zw2ebUCY9cfS9VOoywE,19722
6
6
  test_database.py,sha256=q5ZjI5u20f7ki6iCY5o1iPi0YOvPz1_W0LTDraU8mN4,10040
7
7
  test_display.py,sha256=hHggv-zBthF1BlwWWSjzAm7BBw1DWcElwil5xAuz87g,5822
8
8
  test_failures.py,sha256=H1UxXeVjhJs9-j9yas4vwAha676GX1Es7Kz8RN2B590,6845
9
9
  test_installer.py,sha256=oDMCvaKqqAWjTggltacnasQ-s1gyUvXPDcNrCUGnux4,10216
10
- test_kernel.py,sha256=xw7zpLcZjiRLWvO7vyHQ03_CjmKd_Np8ULhc-WhUV6A,7872
11
- test_launcher.py,sha256=80xVbidrbx8ixMt_x5Uvfn7nFnB637nX69yIZTifyuk,1511
12
- test_prerequisites.py,sha256=5fHtdMSGMf7sjDykIUfOP2h7XOAe-jNhJoZ0Vll5B58,84982
10
+ test_kernel.py,sha256=2EXrLht5ZWdT4N5pb_F3zqZl9NEghjnDpcMGCMw3obI,7917
11
+ test_launcher.py,sha256=8g8CBTvLX64Us4RmHtRPSdpV5E2kQFaudBl7VIsxLhE,1733
12
+ test_prerequisites.py,sha256=-q6v80QXDMB_Mdek2KZTmKfKcRVZnHf8SBEhcT0RyIY,88498
13
13
  test_pstate.py,sha256=a9oAJ9-LANX32XNQhplz6Y75VNYc__QqoSBKIrwvANg,6058
14
- test_s2idle.py,sha256=YpFGpH84xvjI9mY6uBSKapa74hZnG8ZwBShXsJXpmyQ,33540
14
+ test_s2idle.py,sha256=FxsyujgX9Px3m56VzHNeA8yMTHmJiRLWxYt-fh1m5gw,33585
15
15
  test_sleep_report.py,sha256=ANuxYi_C1oSKAi4xUU2wBu4SwJtcZA7VPpazBe3_WUQ,6922
16
- test_validator.py,sha256=-MfrWfhwef_aRqOSD_dJGhH0shsghhtOBgzeijzyLW4,33975
16
+ test_validator.py,sha256=RpjyzxDpExhLcSJfQ0UDuonr4sTFAfa7sTtY5g7tc_Q,36410
17
17
  test_wake.py,sha256=6zi5GVFHQKU1sTWw3O5-aGriB9uu5713QLn4l2wjhpM,7152
18
- amd_debug/__init__.py,sha256=3wZxCDY3KPpfIxMz4vGmp6jUAB2GF4VTK4Xb86vy8c4,1101
18
+ amd_debug/__init__.py,sha256=66Ya61av8RCws6bEY_vdujGmjBIZ6_UqfuWHgMNNOJY,1271
19
19
  amd_debug/acpi.py,sha256=fkD3Sov8cRT5ryPlakRlT7Z9jiCLT9x_MPWxt3xU_tc,3161
20
20
  amd_debug/battery.py,sha256=WN-6ys9PHCZIwg7PdwyBOa62GjBp8WKG0v1YZt5_W5s,3122
21
- amd_debug/bios.py,sha256=nVIDYqyyKiIO21nyS8lV-qB3ypBJOSIKIuVYFOVoBuw,4017
22
- amd_debug/common.py,sha256=H9tIRlRFOMwe0d3f2-vXQeK2rJl5Z1WJzkpQM9ivpOc,10347
21
+ amd_debug/bios.py,sha256=y1iwDqX-mXCkoUtHSi-XO9pN-oLfaqbAMzANGI12zHs,4041
22
+ amd_debug/common.py,sha256=fHrmSEVerVAE7KXjspf60eOAetUO7K9s1yhNyE2xi94,11598
23
23
  amd_debug/database.py,sha256=GkRg3cmaNceyQ2_hy0MBAlMbnTDPHo2co2o4ObWpnQg,10621
24
24
  amd_debug/display.py,sha256=5L9x9tI_UoulHpIvuxuVASRtdXta7UCW_JjTb5StEB0,953
25
25
  amd_debug/failures.py,sha256=z4O4Q-akv3xYGssSZFCqE0cDE4P9F_aw1hxil3McoD4,22910
26
- amd_debug/installer.py,sha256=tNWhlfxQEA30guk-fzMvcc237hf_PARVQuHaH3sTp4A,14287
26
+ amd_debug/installer.py,sha256=6_Y0oHypW-oh_P8N9JW7fzbqidpsi5jphw9_8s5Qvso,14288
27
27
  amd_debug/kernel.py,sha256=HpX-QRh8tgkvqKnExfo2JrYqfcbMY8GNgDrC2VVV0Oc,11638
28
- amd_debug/prerequisites.py,sha256=E5_VpRd2mHiNYQBf13JqsoA5iiJS13wxcH3RERS_qJw,50502
29
- amd_debug/pstate.py,sha256=lLRsayKi7KOXZCQ6Zjm2pNaobpjLXcgLHXZ9Zt40Fd4,9559
28
+ amd_debug/prerequisites.py,sha256=zK-IXL52p_jomA1SsS-6btIHuaapNKW4sD-Fy0wBr68,51399
29
+ amd_debug/pstate.py,sha256=AOKCvUb0ngwHU2C59uSKrFwdLzEyn8r1w2DgWhZAMKM,9583
30
30
  amd_debug/s2idle-hook,sha256=LLiaqPtGd0qetu9n6EYxKHZaIdHpVQDONdOuSc0pfFg,1695
31
- amd_debug/s2idle.py,sha256=lr1wcuJcpvI5pL2gNHqrc7n5E7EYCztvaAYYFPMlGYk,13259
31
+ amd_debug/s2idle.py,sha256=4cxHNfmvq11BE-AtkEthIqR-xrWmxW2LTa6oVDtGztY,13284
32
32
  amd_debug/sleep_report.py,sha256=hhqu711AKtjeYF2xmGcejyCyyPtmq4-gC_hROUCrC0g,17317
33
- amd_debug/validator.py,sha256=-rPqPnYAM1Vevw7vxIbGNPKo1bCRo48IpCBi3Y72-Cw,33419
33
+ amd_debug/test_ttm.py,sha256=McNdEJZ14AeFw8fIxzul9ff8kr67dz1dPvK2fqlM4IA,11219
34
+ amd_debug/ttm.py,sha256=U7beRffgoXIPgUXet4ZMabEGo7b0qqKMluhBVMD7O04,4830
35
+ amd_debug/validator.py,sha256=X-cNFVvHWKzFgT4aR0Td3I2kwQRBOK4vQUk6L276VCQ,34153
34
36
  amd_debug/wake.py,sha256=xT8WrFrN6voCmXWo5dsn4mQ7iR2QJxHrrYBd3EREG-Q,3936
35
37
  amd_debug/bash/amd-s2idle,sha256=g_cle1ElCJpwE4wcLezL6y-BdasDKTnNMhrtzKLE9ks,1142
36
38
  amd_debug/templates/html,sha256=JfGhpmHIB2C2GItdGI1kuC8uayqEVgrpQvAWAj35eZ4,14580
37
39
  amd_debug/templates/md,sha256=r8X2aehnH2gzj0WHYTZ5K9wAqC5y39i_3nkDORSC0uM,787
38
40
  amd_debug/templates/stdout,sha256=hyoOJ96K2dJfnWRWhyCuariLKbEHXvs9mstV_g5aMdI,469
39
41
  amd_debug/templates/txt,sha256=nNdsvbPFOhGdL7VA-_4k5aN3nB-6ouGQt6AsWst7T3w,649
40
- amd_debug_tools-0.2.5.dist-info/licenses/LICENSE,sha256=RBlZI6r3MRGzymI2VDX2iW__D2APDbMhu_Xg5t6BWeo,1066
41
- amd_debug_tools-0.2.5.dist-info/METADATA,sha256=Dkhw4XQncTgjECNEa3Nk6HCYBzqa5lPAS_IbIli7cvA,6877
42
- amd_debug_tools-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
- amd_debug_tools-0.2.5.dist-info/entry_points.txt,sha256=HC11T2up0pPfroAn6Pg5M2jOZXhkWIipToJ1YPTKqu8,116
44
- amd_debug_tools-0.2.5.dist-info/top_level.txt,sha256=XYjxExbUTEtiIlag_5iQvZSVOC1EIxhKM4NLklReQ0k,234
45
- amd_debug_tools-0.2.5.dist-info/RECORD,,
42
+ amd_debug_tools-0.2.6.dist-info/licenses/LICENSE,sha256=RBlZI6r3MRGzymI2VDX2iW__D2APDbMhu_Xg5t6BWeo,1066
43
+ amd_debug_tools-0.2.6.dist-info/METADATA,sha256=pMVZTIUIqoKEksnbkS2UCFJPnDRJ1k6sf3Jwj74mNxk,6971
44
+ amd_debug_tools-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
+ amd_debug_tools-0.2.6.dist-info/entry_points.txt,sha256=hIskDz6k0_6q1qpqWCpVFsca_djxAqkLrUAwzAyEGuE,144
46
+ amd_debug_tools-0.2.6.dist-info/top_level.txt,sha256=XYjxExbUTEtiIlag_5iQvZSVOC1EIxhKM4NLklReQ0k,234
47
+ amd_debug_tools-0.2.6.dist-info/RECORD,,
@@ -2,3 +2,4 @@
2
2
  amd-bios = amd_debug:amd_bios
3
3
  amd-pstate = amd_debug:amd_pstate
4
4
  amd-s2idle = amd_debug:amd_s2idle
5
+ amd-ttm = amd_debug:amd_ttm
test_bios.py CHANGED
@@ -33,8 +33,14 @@ class TestAmdBios(unittest.TestCase):
33
33
  @patch("amd_debug.bios.minimum_kernel")
34
34
  @patch("amd_debug.bios.AcpicaTracer")
35
35
  @patch("amd_debug.bios.print_color")
36
+ @patch("subprocess.run")
36
37
  def test_set_tracing_enable(
37
- self, _mock_print, mock_acpica_tracer, mock_minimum_kernel, mock_relaunch_sudo
38
+ self,
39
+ _mock_run,
40
+ _mock_print,
41
+ mock_acpica_tracer,
42
+ mock_minimum_kernel,
43
+ mock_relaunch_sudo,
38
44
  ):
39
45
  """Test enabling tracing"""
40
46
  mock_minimum_kernel.return_value = True
@@ -53,8 +59,14 @@ class TestAmdBios(unittest.TestCase):
53
59
  @patch("amd_debug.bios.minimum_kernel")
54
60
  @patch("amd_debug.bios.AcpicaTracer")
55
61
  @patch("amd_debug.bios.print_color")
62
+ @patch("subprocess.run")
56
63
  def test_set_tracing_disable(
57
- self, _mock_print, mock_acpica_tracer, mock_minimum_kernel, mock_relaunch_sudo
64
+ self,
65
+ _mock_run,
66
+ _mock_print,
67
+ mock_acpica_tracer,
68
+ mock_minimum_kernel,
69
+ mock_relaunch_sudo,
58
70
  ):
59
71
  """Test disabling tracing"""
60
72
  mock_minimum_kernel.return_value = True
@@ -71,7 +83,10 @@ class TestAmdBios(unittest.TestCase):
71
83
 
72
84
  @patch("amd_debug.bios.sscanf_bios_args")
73
85
  @patch("amd_debug.bios.print_color")
74
- def test_analyze_kernel_log_line(self, mock_print_color, mock_sscanf_bios_args):
86
+ @patch("subprocess.run")
87
+ def test_analyze_kernel_log_line(
88
+ self, _mock_run, mock_print_color, mock_sscanf_bios_args
89
+ ):
75
90
  """Test analyzing kernel log line"""
76
91
  mock_sscanf_bios_args.return_value = "BIOS argument found"
77
92
 
@@ -85,8 +100,9 @@ class TestAmdBios(unittest.TestCase):
85
100
 
86
101
  @patch("amd_debug.bios.sscanf_bios_args")
87
102
  @patch("amd_debug.bios.print_color")
103
+ @patch("subprocess.run")
88
104
  def test_analyze_kernel_log_line_no_bios_args(
89
- self, mock_print_color, mock_sscanf_bios_args
105
+ self, _mock_run, mock_print_color, mock_sscanf_bios_args
90
106
  ):
91
107
  """Test analyzing kernel log line with no BIOS arguments"""
92
108
  mock_sscanf_bios_args.return_value = None
@@ -140,12 +156,12 @@ class TestAmdBios(unittest.TestCase):
140
156
  self.assertFalse(args.enable)
141
157
  self.assertTrue(args.disable)
142
158
 
143
- @patch("sys.argv", ["bios.py", "version"])
159
+ @patch("sys.argv", ["bios.py", "--version"])
144
160
  def test_parse_args_version_command(self):
145
161
  """Test parse_args with version command"""
146
162
 
147
163
  args = parse_args()
148
- self.assertEqual(args.command, "version")
164
+ self.assertTrue(args.version)
149
165
 
150
166
  @patch("sys.argv", ["bios.py"])
151
167
  @patch("argparse.ArgumentParser.print_help")
@@ -227,7 +243,7 @@ class TestAmdBios(unittest.TestCase):
227
243
  self, _mock_print, mock_show_log_info, mock_version, mock_parse_args
228
244
  ):
229
245
  """Test main function with version command"""
230
- mock_parse_args.return_value = argparse.Namespace(command="version")
246
+ mock_parse_args.return_value = argparse.Namespace(version=True, command=None)
231
247
  mock_version.return_value = "1.0.0"
232
248
 
233
249
  result = main()
@@ -241,7 +257,9 @@ class TestAmdBios(unittest.TestCase):
241
257
  @patch("amd_debug.bios.show_log_info")
242
258
  def test_main_invalid_command(self, mock_show_log_info, mock_parse_args):
243
259
  """Test main function with an invalid command"""
244
- mock_parse_args.return_value = argparse.Namespace(command="invalid")
260
+ mock_parse_args.return_value = argparse.Namespace(
261
+ version=False, command="invalid"
262
+ )
245
263
 
246
264
  result = main()
247
265
 
test_common.py CHANGED
@@ -15,6 +15,7 @@ from platform import uname_result
15
15
 
16
16
  from amd_debug.common import (
17
17
  apply_prefix_wrapper,
18
+ bytes_to_gb,
18
19
  Colors,
19
20
  convert_string_to_bool,
20
21
  colorize_choices,
@@ -22,12 +23,15 @@ from amd_debug.common import (
22
23
  compare_file,
23
24
  find_ip_version,
24
25
  fatal_error,
26
+ gb_to_pages,
25
27
  get_distro,
26
28
  get_log_priority,
27
29
  get_pretty_distro,
30
+ get_system_mem,
28
31
  is_root,
29
32
  minimum_kernel,
30
33
  print_color,
34
+ reboot,
31
35
  run_countdown,
32
36
  systemd_in_use,
33
37
  running_ssh,
@@ -442,3 +446,90 @@ class TestCommon(unittest.TestCase):
442
446
  with patch("sys.exit") as mock_exit:
443
447
  convert_string_to_bool("[unclosed_list")
444
448
  mock_exit.assert_called_once_with("Invalid entry: [unclosed_list")
449
+
450
+ def test_bytes_to_gb(self):
451
+ """Test bytes_to_gb conversion"""
452
+ # 4096 bytes should be 4096*4096/(1024*1024*1024) GB
453
+ self.assertAlmostEqual(bytes_to_gb(1), 4096 / (1024 * 1024 * 1024))
454
+ self.assertAlmostEqual(bytes_to_gb(0), 0)
455
+ self.assertAlmostEqual(bytes_to_gb(1024), 1024 * 4096 / (1024 * 1024 * 1024))
456
+
457
+ def test_gb_to_pages(self):
458
+ """Test gb_to_pages conversion"""
459
+ # 1 GB should be int(1 * (1024*1024*1024) / 4096)
460
+ self.assertEqual(gb_to_pages(1), int((1024 * 1024 * 1024) / 4096))
461
+ self.assertEqual(gb_to_pages(0), 0)
462
+ self.assertEqual(gb_to_pages(2), int(2 * (1024 * 1024 * 1024) / 4096))
463
+
464
+ @patch(
465
+ "builtins.open",
466
+ new_callable=mock_open,
467
+ read_data="MemTotal: 16384516 kB\n",
468
+ )
469
+ @patch("os.path.join", return_value="/proc/meminfo")
470
+ def test_get_system_mem_valid(self, _mock_join, mock_file):
471
+ """Test get_system_mem returns correct value"""
472
+ expected_gb = 16384516 / (1024 * 1024)
473
+ self.assertAlmostEqual(get_system_mem(), expected_gb)
474
+ mock_file.assert_called_once_with("/proc/meminfo", "r", encoding="utf-8")
475
+
476
+ @patch("builtins.open", new_callable=mock_open, read_data="NoMemHere: 1234\n")
477
+ @patch("os.path.join", return_value="/proc/meminfo")
478
+ def test_get_system_mem_missing(self, _mock_join, _mock_file):
479
+ """Test get_system_mem raises ValueError if MemTotal is missing"""
480
+ with self.assertRaises(ValueError):
481
+ get_system_mem()
482
+
483
+ @patch("amd_debug.common.fatal_error")
484
+ def test_reboot_importerror(self, mock_fatal_error):
485
+ """Test reboot handles ImportError"""
486
+ with patch.dict("sys.modules", {"dbus": None}):
487
+ reboot()
488
+ mock_fatal_error.assert_called_once_with("Missing dbus")
489
+
490
+ @patch("amd_debug.common.fatal_error")
491
+ def test_reboot_dbus_exception(self, mock_fatal_error):
492
+ """Test reboot handles dbus.exceptions.DBusException"""
493
+
494
+ class DummyDBusException(Exception):
495
+ """Dummy exception"""
496
+
497
+ class DummyIntf:
498
+ """Dummy interface"""
499
+
500
+ def Reboot(self, _arg): # pylint: disable=invalid-name
501
+ """Dummy Reboot method"""
502
+ raise DummyDBusException("fail")
503
+
504
+ class DummyObj: # pylint: disable=too-few-public-methods
505
+ """Dummy object"""
506
+
507
+ def __init__(self):
508
+ pass
509
+
510
+ class DummyBus: # pylint: disable=too-few-public-methods
511
+ """Dummy bus"""
512
+
513
+ def get_object(self, *args, **kwargs):
514
+ """Dummy get_object method"""
515
+ return DummyObj()
516
+
517
+ class DummyDBus:
518
+ """Dummy dbus"""
519
+
520
+ class exceptions: # pylint: disable=invalid-name
521
+ """Dummy exceptions"""
522
+
523
+ DBusException = DummyDBusException
524
+
525
+ def SystemBus(self): # pylint: disable=invalid-name
526
+ """Dummy SystemBus method"""
527
+ return DummyBus()
528
+
529
+ def Interface(self, _obj, _name): # pylint: disable=invalid-name
530
+ """Dummy Interface method"""
531
+ return DummyIntf()
532
+
533
+ with patch.dict("sys.modules", {"dbus": DummyDBus()}):
534
+ reboot()
535
+ self.assertTrue(mock_fatal_error.called)
test_kernel.py CHANGED
@@ -114,7 +114,8 @@ class TestDmesgLogger(unittest.TestCase):
114
114
  """Test Dmesg logger functions"""
115
115
 
116
116
  @classmethod
117
- def setUpClass(cls):
117
+ @patch("subprocess.run")
118
+ def setUpClass(cls, _mock_run=None):
118
119
  logging.basicConfig(filename="/dev/null", level=logging.DEBUG)
119
120
 
120
121
  def test_dmesg_logger_initialization(self):
test_launcher.py CHANGED
@@ -52,3 +52,10 @@ class TestLauncher(unittest.TestCase):
52
52
  with patch("amd_debug.pstate.main") as mock_main:
53
53
  amd_debug.launch_tool("amd_pstate.py")
54
54
  mock_main.assert_called_once()
55
+
56
+ def test_launcher_amd_ttm(self):
57
+ """Test launching amd_ttm"""
58
+
59
+ with patch("amd_debug.ttm.main") as mock_main:
60
+ amd_debug.launch_tool("amd_ttm.py")
61
+ mock_main.assert_called_once()
test_prerequisites.py CHANGED
@@ -326,7 +326,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
326
326
  BIT(9) | 1
327
327
  ) # Kernel warnings ignored, other taint present
328
328
  result = self.validator.check_taint()
329
- self.assertFalse(result)
329
+ self.assertTrue(result)
330
330
  self.assertTrue(
331
331
  any(isinstance(f, TaintedKernel) for f in self.validator.failures)
332
332
  )
@@ -1959,3 +1959,79 @@ class TestPrerequisiteValidator(unittest.TestCase):
1959
1959
  result = self.validator.check_dpia_pg_dmcub()
1960
1960
  self.assertTrue(result)
1961
1961
  self.mock_db.record_prereq.assert_not_called()
1962
+
1963
+ @patch("amd_debug.prerequisites.os.path.exists")
1964
+ def test_capture_nvidia_version_file_missing(self, mock_exists):
1965
+ """Test capture_nvidia when /proc/driver/nvidia/version does not exist"""
1966
+ mock_exists.side_effect = lambda p: False if "version" in p else True
1967
+ result = self.validator.capture_nvidia()
1968
+ self.assertTrue(result)
1969
+ self.mock_db.record_debug_file.assert_not_called()
1970
+ self.mock_db.record_prereq.assert_not_called()
1971
+
1972
+ @patch("amd_debug.prerequisites.os.path.exists")
1973
+ def test_capture_nvidia_gpus_dir_missing(self, mock_exists):
1974
+ """Test capture_nvidia when /proc/driver/nvidia/gpus does not exist"""
1975
+
1976
+ def exists_side_effect(path):
1977
+ if "version" in path:
1978
+ return True
1979
+ if "gpus" in path:
1980
+ return False
1981
+ return True
1982
+
1983
+ mock_exists.side_effect = exists_side_effect
1984
+ result = self.validator.capture_nvidia()
1985
+ self.assertTrue(result)
1986
+ self.mock_db.record_debug_file.assert_called_once_with(
1987
+ "/proc/driver/nvidia/version"
1988
+ )
1989
+ self.mock_db.record_prereq.assert_not_called()
1990
+
1991
+ @patch("amd_debug.prerequisites.os.walk")
1992
+ @patch("amd_debug.prerequisites.os.path.exists")
1993
+ def test_capture_nvidia_success(self, mock_exists, mock_walk):
1994
+ """Test capture_nvidia when NVIDIA GPU files are present and readable"""
1995
+ mock_exists.side_effect = lambda p: True
1996
+ mock_walk.return_value = [
1997
+ ("/proc/driver/nvidia/gpus/0000:01:00.0", [], ["info", "power"])
1998
+ ]
1999
+ result = self.validator.capture_nvidia()
2000
+ self.assertTrue(result)
2001
+ self.mock_db.record_debug_file.assert_any_call("/proc/driver/nvidia/version")
2002
+ self.mock_db.record_debug.assert_any_call("NVIDIA info")
2003
+ self.mock_db.record_debug_file.assert_any_call(
2004
+ "/proc/driver/nvidia/gpus/0000:01:00.0/info"
2005
+ )
2006
+ self.mock_db.record_debug.assert_any_call("NVIDIA power")
2007
+ self.mock_db.record_debug_file.assert_any_call(
2008
+ "/proc/driver/nvidia/gpus/0000:01:00.0/power"
2009
+ )
2010
+
2011
+ @patch("amd_debug.prerequisites.os.walk")
2012
+ @patch("amd_debug.prerequisites.os.path.exists")
2013
+ def test_capture_nvidia_permission_error_on_version(self, mock_exists, mock_walk):
2014
+ """Test capture_nvidia when PermissionError occurs reading version file"""
2015
+ mock_exists.side_effect = lambda p: True if "version" in p else False
2016
+ self.mock_db.record_debug_file.side_effect = PermissionError
2017
+ result = self.validator.capture_nvidia()
2018
+ self.assertTrue(result)
2019
+ self.mock_db.record_prereq.assert_called_with(
2020
+ "NVIDIA GPU version not readable", "👀"
2021
+ )
2022
+
2023
+ @patch("amd_debug.prerequisites.os.walk")
2024
+ @patch("amd_debug.prerequisites.os.path.exists")
2025
+ def test_capture_nvidia_permission_error_on_gpu_file(self, mock_exists, mock_walk):
2026
+ """Test capture_nvidia when PermissionError occurs reading a GPU file"""
2027
+ mock_exists.side_effect = lambda p: True
2028
+ mock_walk.return_value = [
2029
+ ("/proc/driver/nvidia/gpus/0000:01:00.0", [], ["info"])
2030
+ ]
2031
+ self.mock_db.record_debug_file.side_effect = [None, PermissionError]
2032
+ result = self.validator.capture_nvidia()
2033
+ self.assertTrue(result)
2034
+ self.mock_db.record_debug.assert_any_call("NVIDIA info")
2035
+ self.mock_db.record_prereq.assert_called_with(
2036
+ "NVIDIA GPU {f} not readable", "👀"
2037
+ )
test_s2idle.py CHANGED
@@ -117,9 +117,9 @@ class TestParseArgs(unittest.TestCase):
117
117
 
118
118
  def test_version_command(self):
119
119
  """Test parse_args with version command"""
120
- sys.argv = ["s2idle.py", "version"]
120
+ sys.argv = ["s2idle.py", "--version"]
121
121
  args = parse_args()
122
- self.assertEqual(args.action, "version")
122
+ self.assertTrue(args.version)
123
123
 
124
124
 
125
125
  class TestMainFunction(unittest.TestCase):
@@ -214,7 +214,7 @@ class TestMainFunction(unittest.TestCase):
214
214
  """Test main function with version action"""
215
215
  sys.argv = ["s2idle.py", "version"]
216
216
  with patch("amd_debug.s2idle.parse_args") as mock_parse_args:
217
- mock_parse_args.return_value = argparse.Namespace(action="version")
217
+ mock_parse_args.return_value = argparse.Namespace(version=True, action=None)
218
218
  mock_version.return_value = "1.0.0"
219
219
  with patch("builtins.print") as mock_print:
220
220
  result = main()
@@ -226,7 +226,9 @@ class TestMainFunction(unittest.TestCase):
226
226
  """Test main function with no action specified"""
227
227
  sys.argv = ["s2idle.py"]
228
228
  with patch("amd_debug.s2idle.parse_args") as mock_parse_args:
229
- mock_parse_args.return_value = argparse.Namespace(action=None)
229
+ mock_parse_args.return_value = argparse.Namespace(
230
+ version=False, action=None
231
+ )
230
232
  with self.assertRaises(SystemExit) as cm:
231
233
  main()
232
234
  self.assertEqual(cm.exception.code, "no action specified")
test_validator.py CHANGED
@@ -71,7 +71,8 @@ class TestValidator(unittest.TestCase):
71
71
  logging.basicConfig(filename="/dev/null", level=logging.DEBUG)
72
72
 
73
73
  @patch("amd_debug.validator.SleepDatabase")
74
- def setUp(self, _db_mock):
74
+ @patch("subprocess.run")
75
+ def setUp(self, _db_mock, _mock_run):
75
76
  """Set up a mock context for testing"""
76
77
  self.validator = SleepValidator(tool_debug=True, bios_debug=False)
77
78
 
@@ -830,3 +831,64 @@ class TestValidator(unittest.TestCase):
830
831
  )
831
832
  mock_os_write.assert_called_once_with(3, b"mem")
832
833
  mock_os_close.assert_called_once_with(3)
834
+
835
+ @patch("os.path.exists")
836
+ @patch("os.open")
837
+ @patch("os.write")
838
+ @patch("os.close")
839
+ def test_toggle_nvidia_file_not_exists(
840
+ self, mock_close, mock_write, mock_open, mock_exists
841
+ ):
842
+ """Test toggle_nvidia returns True if NVIDIA suspend file does not exist"""
843
+ mock_exists.return_value = False
844
+ result = self.validator.toggle_nvidia(b"suspend")
845
+ self.assertTrue(result)
846
+ mock_open.assert_not_called()
847
+ mock_write.assert_not_called()
848
+ mock_close.assert_not_called()
849
+
850
+ @patch("os.path.exists")
851
+ @patch("os.open")
852
+ @patch("os.write")
853
+ @patch("os.close")
854
+ def test_toggle_nvidia_success(
855
+ self, mock_close, mock_write, mock_open, mock_exists
856
+ ):
857
+ """Test toggle_nvidia writes value and returns True on success"""
858
+ mock_exists.return_value = True
859
+ mock_open.return_value = 42
860
+ mock_write.return_value = None
861
+ with patch.object(self.validator.db, "record_debug") as mock_record_debug:
862
+ result = self.validator.toggle_nvidia(b"suspend")
863
+ self.assertTrue(result)
864
+ mock_open.assert_called_once_with(
865
+ "/proc/driver/nvidia/suspend", os.O_WRONLY | os.O_SYNC
866
+ )
867
+ mock_write.assert_called_once_with(42, b"suspend")
868
+ mock_close.assert_called_once_with(42)
869
+ mock_record_debug.assert_called_once_with(
870
+ "Wrote b'suspend' to NVIDIA driver"
871
+ )
872
+
873
+ @patch("os.path.exists")
874
+ @patch("os.open")
875
+ @patch("os.write")
876
+ @patch("os.close")
877
+ def test_toggle_nvidia_oserror(
878
+ self, mock_close, mock_write, mock_open, mock_exists
879
+ ):
880
+ """Test toggle_nvidia handles OSError and returns False"""
881
+ mock_exists.return_value = True
882
+ mock_open.return_value = 99
883
+ mock_write.side_effect = OSError("write error")
884
+ with patch.object(
885
+ self.validator.db, "record_cycle_data"
886
+ ) as mock_record_cycle_data:
887
+ result = self.validator.toggle_nvidia(b"resume")
888
+ self.assertFalse(result)
889
+ mock_open.assert_called_once_with(
890
+ "/proc/driver/nvidia/suspend", os.O_WRONLY | os.O_SYNC
891
+ )
892
+ mock_write.assert_called_once_with(99, b"resume")
893
+ mock_close.assert_called_once_with(99)
894
+ mock_record_cycle_data.assert_called_once()