amd-debug-tools 0.2.1__tar.gz → 0.2.2__tar.gz
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_tools-0.2.1 → amd_debug_tools-0.2.2}/PKG-INFO +4 -3
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/README.md +3 -2
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/common.py +24 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/failures.py +13 -1
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/installer.py +34 -2
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/prerequisites.py +87 -24
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/s2idle.py +21 -7
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/sleep_report.py +2 -2
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/md +0 -7
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/validator.py +3 -5
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/PKG-INFO +4 -3
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_common.py +112 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_installer.py +8 -6
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_prerequisites.py +194 -28
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_s2idle.py +55 -20
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_validator.py +3 -5
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/LICENSE +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/pyproject.toml +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/setup.cfg +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/__init__.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/acpi.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/bash/amd-s2idle +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/battery.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/bios.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/database.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/display.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/kernel.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/pstate.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/s2idle-hook +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/html +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/stdout +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/txt +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/wake.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/SOURCES.txt +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/dependency_links.txt +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/entry_points.txt +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/requires.txt +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/top_level.txt +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/launcher.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_acpi.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_batteries.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_bios.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_database.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_display.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_failures.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_kernel.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_launcher.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_pstate.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_sleep_report.py +0 -0
- {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_wake.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amd-debug-tools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: debug tools for AMD systems
|
|
5
5
|
Author-email: Mario Limonciello <superm1@kernel.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -125,8 +125,9 @@ The following optional arguments are supported for this command:
|
|
|
125
125
|
--format FORMAT Format of the report to produce (html, txt or md)
|
|
126
126
|
--report-file File to write the report to
|
|
127
127
|
--tool-debug Enable tool debug logging
|
|
128
|
-
--report-debug
|
|
129
|
-
|
|
128
|
+
--report-debug
|
|
129
|
+
--no-report-debug
|
|
130
|
+
Include debug messages in report (WARNING: can significantly increase report size)
|
|
130
131
|
If the tool is launched with an environment that can call `xdg-open`, the report
|
|
131
132
|
will be opened in a browser.
|
|
132
133
|
|
|
@@ -101,8 +101,9 @@ The following optional arguments are supported for this command:
|
|
|
101
101
|
--format FORMAT Format of the report to produce (html, txt or md)
|
|
102
102
|
--report-file File to write the report to
|
|
103
103
|
--tool-debug Enable tool debug logging
|
|
104
|
-
--report-debug
|
|
105
|
-
|
|
104
|
+
--report-debug
|
|
105
|
+
--no-report-debug
|
|
106
|
+
Include debug messages in report (WARNING: can significantly increase report size)
|
|
106
107
|
If the tool is launched with an environment that can call `xdg-open`, the report
|
|
107
108
|
will be opened in a browser.
|
|
108
109
|
|
|
@@ -14,6 +14,7 @@ import struct
|
|
|
14
14
|
import subprocess
|
|
15
15
|
import re
|
|
16
16
|
import sys
|
|
17
|
+
from ast import literal_eval
|
|
17
18
|
from datetime import date, timedelta
|
|
18
19
|
|
|
19
20
|
|
|
@@ -278,6 +279,19 @@ def get_property_pyudev(properties, key, fallback=""):
|
|
|
278
279
|
return ""
|
|
279
280
|
|
|
280
281
|
|
|
282
|
+
def find_ip_version(base_path, kind, hw_ver) -> bool:
|
|
283
|
+
"""Determine if an IP version is present on the system"""
|
|
284
|
+
b = os.path.join(base_path, "ip_discovery", "die", "0", kind, "0")
|
|
285
|
+
for key, expected_value in hw_ver.items():
|
|
286
|
+
p = os.path.join(b, key)
|
|
287
|
+
if not os.path.exists(p):
|
|
288
|
+
return False
|
|
289
|
+
v = int(read_file(p))
|
|
290
|
+
if v != expected_value:
|
|
291
|
+
return False
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
|
|
281
295
|
def read_msr(msr, cpu):
|
|
282
296
|
"""Read a Model-Specific Register (MSR) value from the CPU."""
|
|
283
297
|
p = f"/dev/cpu/{cpu}/msr"
|
|
@@ -308,6 +322,16 @@ def running_ssh():
|
|
|
308
322
|
return "SSH_CLIENT" in os.environ or "SSH_TTY" in os.environ
|
|
309
323
|
|
|
310
324
|
|
|
325
|
+
def convert_string_to_bool(str_value) -> bool:
|
|
326
|
+
"""convert a string to a boolean value"""
|
|
327
|
+
try:
|
|
328
|
+
value = literal_eval(str_value)
|
|
329
|
+
except (SyntaxError, ValueError):
|
|
330
|
+
value = None
|
|
331
|
+
sys.exit(f"Invalid entry: {str_value}")
|
|
332
|
+
return bool(value)
|
|
333
|
+
|
|
334
|
+
|
|
311
335
|
def _git_describe() -> str:
|
|
312
336
|
"""Get the git description of the current commit"""
|
|
313
337
|
try:
|
|
@@ -416,7 +416,7 @@ class LowHardwareSleepResidency(S0i3Failure):
|
|
|
416
416
|
super().__init__()
|
|
417
417
|
self.description = "System had low hardware sleep residency"
|
|
418
418
|
self.explanation = (
|
|
419
|
-
f"The system was asleep for {timedelta(seconds=duration)}, but only spent {percent:.2%} "
|
|
419
|
+
f"The system was asleep for {timedelta(seconds=duration)}, but only spent {percent/100:.2%} "
|
|
420
420
|
"of this time in a hardware sleep state. In sleep cycles that are at least "
|
|
421
421
|
"60 seconds long it's expected you spend above 90 percent of the cycle in "
|
|
422
422
|
"hardware sleep."
|
|
@@ -586,3 +586,15 @@ class RogAllyMcuPowerSave(S0i3Failure):
|
|
|
586
586
|
"The MCU powersave feature is disabled which will cause problems "
|
|
587
587
|
"with the controller after suspend/resume."
|
|
588
588
|
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class DmcubTooOld(S0i3Failure):
|
|
592
|
+
"""DMCUB microcode is too old"""
|
|
593
|
+
|
|
594
|
+
def __init__(self, current, expected):
|
|
595
|
+
super().__init__()
|
|
596
|
+
self.description = "DMCUB microcode is too old"
|
|
597
|
+
self.explanation = (
|
|
598
|
+
f"The DMCUB microcode version {hex(current)} is older than the"
|
|
599
|
+
f"minimum suggested version {hex(expected)}."
|
|
600
|
+
)
|
|
@@ -26,6 +26,7 @@ class Headers: # pylint: disable=too-few-public-methods
|
|
|
26
26
|
|
|
27
27
|
MissingIasl = "ACPI extraction tool `iasl` is missing"
|
|
28
28
|
MissingEdidDecode = "EDID decoding tool `edid-decode` is missing"
|
|
29
|
+
MissingDiEdidDecode = "EDID decoding tool `di-edid-decode` is missing"
|
|
29
30
|
MissingEthtool = "Ethtool is missing"
|
|
30
31
|
InstallAction = "Attempting to install"
|
|
31
32
|
MissingFwupd = "Firmware update library `fwupd` is missing"
|
|
@@ -35,6 +36,7 @@ class Headers: # pylint: disable=too-few-public-methods
|
|
|
35
36
|
MissingTabulate = "Data library `tabulate` is missing"
|
|
36
37
|
MissingJinja2 = "Template library `jinja2` is missing"
|
|
37
38
|
MissingSeaborn = "Data visualization library `seaborn` is missing"
|
|
39
|
+
UnknownDistro = "No distro installation support available, install manually"
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
class DistroPackage:
|
|
@@ -71,7 +73,8 @@ class DistroPackage:
|
|
|
71
73
|
return False
|
|
72
74
|
installer = ["pacman", "-Sy", self.arch]
|
|
73
75
|
else:
|
|
74
|
-
|
|
76
|
+
print_color(Headers.UnknownDistro, "👀")
|
|
77
|
+
return True
|
|
75
78
|
|
|
76
79
|
try:
|
|
77
80
|
subprocess.check_call(installer)
|
|
@@ -188,6 +191,18 @@ class EdidDecodePackage(DistroPackage):
|
|
|
188
191
|
)
|
|
189
192
|
|
|
190
193
|
|
|
194
|
+
class DisplayInfoPackage(DistroPackage):
|
|
195
|
+
"""display info package"""
|
|
196
|
+
|
|
197
|
+
def __init__(self):
|
|
198
|
+
super().__init__(
|
|
199
|
+
deb="libdisplay-info-bin",
|
|
200
|
+
rpm="libdisplay-info",
|
|
201
|
+
arch="libdisplay-info",
|
|
202
|
+
message=Headers.MissingDiEdidDecode,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
191
206
|
class FwupdPackage(DistroPackage):
|
|
192
207
|
"""Fwupd package"""
|
|
193
208
|
|
|
@@ -261,7 +276,19 @@ class Installer(AmdTool):
|
|
|
261
276
|
package = EthtoolPackage()
|
|
262
277
|
if not package.install():
|
|
263
278
|
return False
|
|
279
|
+
# can be satisified by either edid-decode or di-edid-decode
|
|
264
280
|
if "edid-decode" in self.requirements:
|
|
281
|
+
try:
|
|
282
|
+
di_edid = (
|
|
283
|
+
subprocess.call(
|
|
284
|
+
["di-edid-decode", "--help"],
|
|
285
|
+
stdout=subprocess.DEVNULL,
|
|
286
|
+
stderr=subprocess.DEVNULL,
|
|
287
|
+
)
|
|
288
|
+
== 255
|
|
289
|
+
)
|
|
290
|
+
except FileNotFoundError:
|
|
291
|
+
di_edid = False
|
|
265
292
|
try:
|
|
266
293
|
edid = (
|
|
267
294
|
subprocess.call(
|
|
@@ -271,7 +298,12 @@ class Installer(AmdTool):
|
|
|
271
298
|
)
|
|
272
299
|
except FileNotFoundError:
|
|
273
300
|
edid = False
|
|
274
|
-
if not edid:
|
|
301
|
+
if not di_edid and not edid:
|
|
302
|
+
# try to install di-edid-decode first
|
|
303
|
+
package = DisplayInfoPackage()
|
|
304
|
+
if package.install():
|
|
305
|
+
return True
|
|
306
|
+
# fall back to edid-decode instead
|
|
275
307
|
package = EdidDecodePackage()
|
|
276
308
|
if not package.install():
|
|
277
309
|
return False
|
|
@@ -26,6 +26,7 @@ from amd_debug.common import (
|
|
|
26
26
|
apply_prefix_wrapper,
|
|
27
27
|
BIT,
|
|
28
28
|
clear_temporary_message,
|
|
29
|
+
find_ip_version,
|
|
29
30
|
get_distro,
|
|
30
31
|
get_pretty_distro,
|
|
31
32
|
get_property_pyudev,
|
|
@@ -48,6 +49,7 @@ from amd_debug.failures import (
|
|
|
48
49
|
DevSlpDiskIssue,
|
|
49
50
|
DevSlpHostIssue,
|
|
50
51
|
DMArNotEnabled,
|
|
52
|
+
DmcubTooOld,
|
|
51
53
|
DmiNotSetup,
|
|
52
54
|
FadtWrong,
|
|
53
55
|
I2CHidBug,
|
|
@@ -134,20 +136,24 @@ class PrerequisiteValidator(AmdTool):
|
|
|
134
136
|
return True
|
|
135
137
|
|
|
136
138
|
for name, p in edids.items():
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
139
|
+
output = None
|
|
140
|
+
for cmd in ["di-edid-decode", "edid-decode"]:
|
|
141
|
+
try:
|
|
142
|
+
cmd = ["edid-decode", p]
|
|
143
|
+
output = subprocess.check_output(
|
|
144
|
+
cmd, stderr=subprocess.DEVNULL
|
|
145
|
+
).decode("utf-8")
|
|
146
|
+
break
|
|
147
|
+
except FileNotFoundError:
|
|
148
|
+
self.db.record_debug(f"{cmd} not installed")
|
|
149
|
+
except subprocess.CalledProcessError as e:
|
|
150
|
+
self.db.record_debug(
|
|
151
|
+
f"failed to capture edid with {cmd}: {e.output}"
|
|
152
|
+
)
|
|
153
|
+
if not output:
|
|
154
|
+
self.db.record_prereq("Failed to capture EDID table", "👀")
|
|
155
|
+
else:
|
|
156
|
+
self.db.record_debug(apply_prefix_wrapper(f"EDID for {name}:", output))
|
|
151
157
|
return True
|
|
152
158
|
|
|
153
159
|
def check_amdgpu(self):
|
|
@@ -395,6 +401,53 @@ class PrerequisiteValidator(AmdTool):
|
|
|
395
401
|
)
|
|
396
402
|
return True
|
|
397
403
|
|
|
404
|
+
def check_dpia_pg_dmcub(self):
|
|
405
|
+
"""Check if DMUB is new enough to PG DPIA when no USB4 present"""
|
|
406
|
+
usb4_found = False
|
|
407
|
+
for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0340"):
|
|
408
|
+
usb4_found = True
|
|
409
|
+
break
|
|
410
|
+
if usb4_found:
|
|
411
|
+
self.db.record_debug("USB4 routers found, no need to check DMCUB version")
|
|
412
|
+
return True
|
|
413
|
+
# Check if matching DCN present
|
|
414
|
+
for device in self.pyudev.list_devices(subsystem="pci"):
|
|
415
|
+
current = None
|
|
416
|
+
klass = device.properties.get("PCI_CLASS")
|
|
417
|
+
if klass not in ["30000", "38000"]:
|
|
418
|
+
continue
|
|
419
|
+
pci_id = device.properties.get("PCI_ID")
|
|
420
|
+
if not pci_id.startswith("1002"):
|
|
421
|
+
continue
|
|
422
|
+
hw_ver = {"major": 3, "minor": 5, "revision": 0}
|
|
423
|
+
if not find_ip_version(device.sys_path, "DMU", hw_ver):
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# DCN was found, lookup version from sysfs
|
|
427
|
+
p = os.path.join(device.sys_path, "fw_version", "dmcub_fw_version")
|
|
428
|
+
if os.path.exists(p):
|
|
429
|
+
current = int(read_file(p), 16)
|
|
430
|
+
|
|
431
|
+
# no sysfs, try to look for version from debugfs
|
|
432
|
+
if not current:
|
|
433
|
+
slot = device.properties["PCI_SLOT_NAME"]
|
|
434
|
+
p = os.path.join(
|
|
435
|
+
"/", "sys", "kernel", "debug", "dri", slot, "amdgpu_firmware_info"
|
|
436
|
+
)
|
|
437
|
+
contents = read_file(p)
|
|
438
|
+
for line in contents.split("\n"):
|
|
439
|
+
if not line.startswith("DMCUB"):
|
|
440
|
+
continue
|
|
441
|
+
current = int(line.split()[-1], 16)
|
|
442
|
+
if current:
|
|
443
|
+
expected = 0x09001B00
|
|
444
|
+
if current >= expected:
|
|
445
|
+
return True
|
|
446
|
+
self.db.record_prereq("DMCUB Firmware is outdated", "❌")
|
|
447
|
+
self.failures += [DmcubTooOld(current, expected)]
|
|
448
|
+
return False
|
|
449
|
+
return True
|
|
450
|
+
|
|
398
451
|
def check_usb4(self):
|
|
399
452
|
"""Check if the thunderbolt driver is loaded"""
|
|
400
453
|
slots = []
|
|
@@ -698,9 +751,13 @@ class PrerequisiteValidator(AmdTool):
|
|
|
698
751
|
interface = device.properties.get("INTERFACE")
|
|
699
752
|
cmd = ["ethtool", interface]
|
|
700
753
|
wol_supported = False
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
754
|
+
try:
|
|
755
|
+
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode(
|
|
756
|
+
"utf-8"
|
|
757
|
+
)
|
|
758
|
+
except FileNotFoundError:
|
|
759
|
+
self.db.record_prereq(f"ethtool is missing", "👀")
|
|
760
|
+
return True
|
|
704
761
|
for line in output.split("\n"):
|
|
705
762
|
if "Supports Wake-on" in line:
|
|
706
763
|
val = line.split(":")[1].strip()
|
|
@@ -949,6 +1006,8 @@ class PrerequisiteValidator(AmdTool):
|
|
|
949
1006
|
stderr=subprocess.DEVNULL,
|
|
950
1007
|
)
|
|
951
1008
|
self.db.record_debug_file(f"{prefix}.dsl")
|
|
1009
|
+
except FileNotFoundError as e:
|
|
1010
|
+
self.db.record_prereq(f"Failed to capture ACPI table: {e}", "👀")
|
|
952
1011
|
except subprocess.CalledProcessError as e:
|
|
953
1012
|
self.db.record_prereq(
|
|
954
1013
|
f"Failed to capture ACPI table: {e.output}", "👀"
|
|
@@ -1099,13 +1158,16 @@ class PrerequisiteValidator(AmdTool):
|
|
|
1099
1158
|
self.db.record_prereq("IOMMU disabled", "✅")
|
|
1100
1159
|
return True
|
|
1101
1160
|
debug_str += "DMA protection:\n"
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1161
|
+
p = os.path.join("/", "sys", "firmware", "acpi", "tables", "IVRS")
|
|
1162
|
+
with open(p, "rb") as f:
|
|
1163
|
+
data = f.read()
|
|
1164
|
+
if len(data) < 40:
|
|
1165
|
+
raise ValueError(
|
|
1166
|
+
"IVRS table appears too small to contain virtualization info."
|
|
1167
|
+
)
|
|
1168
|
+
virt_info = struct.unpack_from("I", data, 36)[0]
|
|
1169
|
+
debug_str += f"Virtualization info: 0x{virt_info:x}"
|
|
1170
|
+
found_dmar = (virt_info & 0x2) != 0
|
|
1109
1171
|
self.db.record_debug(debug_str)
|
|
1110
1172
|
if not found_dmar:
|
|
1111
1173
|
self.db.record_prereq(
|
|
@@ -1205,6 +1267,7 @@ class PrerequisiteValidator(AmdTool):
|
|
|
1205
1267
|
self.check_smt,
|
|
1206
1268
|
self.check_iommu,
|
|
1207
1269
|
self.check_asus_rog_ally,
|
|
1270
|
+
self.check_dpia_pg_dmcub,
|
|
1208
1271
|
]
|
|
1209
1272
|
|
|
1210
1273
|
checks += [
|
|
@@ -9,6 +9,7 @@ import sqlite3
|
|
|
9
9
|
|
|
10
10
|
from datetime import date, timedelta, datetime
|
|
11
11
|
from amd_debug.common import (
|
|
12
|
+
convert_string_to_bool,
|
|
12
13
|
colorize_choices,
|
|
13
14
|
is_root,
|
|
14
15
|
relaunch_sudo,
|
|
@@ -32,6 +33,7 @@ class Defaults:
|
|
|
32
33
|
since = date.today() - timedelta(days=60)
|
|
33
34
|
until = date.today() + timedelta(days=1)
|
|
34
35
|
format_choices = ["txt", "md", "html", "stdout"]
|
|
36
|
+
boolean_choices = ["true", "false"]
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class Headers:
|
|
@@ -47,6 +49,7 @@ class Headers:
|
|
|
47
49
|
FormatDescription = "What format to output the report in"
|
|
48
50
|
MaxDurationDescription = "What is the maximum suspend cycle length (seconds)"
|
|
49
51
|
MaxWaitDescription = "What is the maximum time between suspend cycles (seconds)"
|
|
52
|
+
ReportDebugDescription = "Enable debug output in report (increased size)"
|
|
50
53
|
|
|
51
54
|
|
|
52
55
|
def display_report_file(fname, fmt) -> None:
|
|
@@ -85,7 +88,7 @@ def get_report_format() -> str:
|
|
|
85
88
|
return "html"
|
|
86
89
|
|
|
87
90
|
|
|
88
|
-
def prompt_report_arguments(since, until, fname, fmt) -> str:
|
|
91
|
+
def prompt_report_arguments(since, until, fname, fmt, report_debug) -> str:
|
|
89
92
|
"""Prompt user for report configuration"""
|
|
90
93
|
if not since:
|
|
91
94
|
default = Defaults.since
|
|
@@ -114,7 +117,16 @@ def prompt_report_arguments(since, until, fname, fmt) -> str:
|
|
|
114
117
|
fmt = get_report_format()
|
|
115
118
|
if fmt not in Defaults.format_choices:
|
|
116
119
|
sys.exit(f"Invalid format: {fmt}")
|
|
117
|
-
|
|
120
|
+
if report_debug is None:
|
|
121
|
+
inp = (
|
|
122
|
+
input(
|
|
123
|
+
f"{Headers.ReportDebugDescription} ({colorize_choices(Defaults.boolean_choices, "true")})? "
|
|
124
|
+
)
|
|
125
|
+
.lower()
|
|
126
|
+
.capitalize()
|
|
127
|
+
)
|
|
128
|
+
report_debug = True if not inp else convert_string_to_bool(inp)
|
|
129
|
+
return [since, until, get_report_file(fname, fmt), fmt, report_debug]
|
|
118
130
|
|
|
119
131
|
|
|
120
132
|
def prompt_test_arguments(duration, wait, count, rand) -> list:
|
|
@@ -157,7 +169,9 @@ def prompt_test_arguments(duration, wait, count, rand) -> list:
|
|
|
157
169
|
def report(since, until, fname, fmt, tool_debug, report_debug) -> bool:
|
|
158
170
|
"""Generate a report from previous sleep cycles"""
|
|
159
171
|
try:
|
|
160
|
-
since, until, fname, fmt = prompt_report_arguments(
|
|
172
|
+
since, until, fname, fmt, report_debug = prompt_report_arguments(
|
|
173
|
+
since, until, fname, fmt, report_debug
|
|
174
|
+
)
|
|
161
175
|
except KeyboardInterrupt:
|
|
162
176
|
sys.exit("\nReport generation cancelled")
|
|
163
177
|
try:
|
|
@@ -209,8 +223,8 @@ def run_test_cycle(
|
|
|
209
223
|
app = SleepValidator(tool_debug=debug, bios_debug=bios_debug)
|
|
210
224
|
try:
|
|
211
225
|
duration, wait, count = prompt_test_arguments(duration, wait, count, rand)
|
|
212
|
-
since, until, fname, fmt = prompt_report_arguments(
|
|
213
|
-
datetime.now().isoformat(), Defaults.until.isoformat(), fname, fmt
|
|
226
|
+
since, until, fname, fmt, report_debug = prompt_report_arguments(
|
|
227
|
+
datetime.now().isoformat(), Defaults.until.isoformat(), fname, fmt, True
|
|
214
228
|
)
|
|
215
229
|
except KeyboardInterrupt:
|
|
216
230
|
sys.exit("\nTest cancelled")
|
|
@@ -229,7 +243,7 @@ def run_test_cycle(
|
|
|
229
243
|
fname=fname,
|
|
230
244
|
fmt=fmt,
|
|
231
245
|
tool_debug=debug,
|
|
232
|
-
report_debug=
|
|
246
|
+
report_debug=report_debug,
|
|
233
247
|
)
|
|
234
248
|
app.run()
|
|
235
249
|
|
|
@@ -347,7 +361,7 @@ def parse_args():
|
|
|
347
361
|
)
|
|
348
362
|
report_cmd.add_argument(
|
|
349
363
|
"--report-debug",
|
|
350
|
-
action=
|
|
364
|
+
action=argparse.BooleanOptionalAction,
|
|
351
365
|
help="Include debug messages in report (WARNING: can significantly increase report size)",
|
|
352
366
|
)
|
|
353
367
|
|
|
@@ -432,8 +432,8 @@ class SleepReport(AmdTool):
|
|
|
432
432
|
|
|
433
433
|
characters = print_temporary_message("Building report, please wait...")
|
|
434
434
|
|
|
435
|
-
# Build charts in the page for html
|
|
436
|
-
if len(self.df.index) > 1 and
|
|
435
|
+
# Build charts in the page for html format
|
|
436
|
+
if len(self.df.index) > 1 and self.format == "html":
|
|
437
437
|
self.build_battery_chart()
|
|
438
438
|
self.build_hw_sleep_chart()
|
|
439
439
|
|
|
@@ -469,12 +469,10 @@ class SleepValidator(AmdTool):
|
|
|
469
469
|
)
|
|
470
470
|
return False
|
|
471
471
|
except FileNotFoundError:
|
|
472
|
-
self.db.
|
|
473
|
-
return False
|
|
472
|
+
self.db.record_debug(f"HW sleep statistics file {p} is missing")
|
|
474
473
|
if not self.hw_sleep_duration:
|
|
475
474
|
self.db.record_cycle_data("Did not reach hardware sleep state", "❌")
|
|
476
|
-
|
|
477
|
-
return self.hw_sleep_duration is not None
|
|
475
|
+
return self.hw_sleep_duration
|
|
478
476
|
|
|
479
477
|
def capture_command_line(self):
|
|
480
478
|
"""Capture the kernel command line to debug"""
|
|
@@ -674,7 +672,7 @@ class SleepValidator(AmdTool):
|
|
|
674
672
|
def prep(self):
|
|
675
673
|
"""Prepare the system for suspend testing"""
|
|
676
674
|
self.last_suspend = datetime.now()
|
|
677
|
-
self.kernel_log.seek_tail()
|
|
675
|
+
self.kernel_log.seek_tail(self.last_suspend)
|
|
678
676
|
self.db.start_cycle(self.last_suspend)
|
|
679
677
|
self.kernel_duration = 0
|
|
680
678
|
self.hw_sleep_duration = 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amd-debug-tools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: debug tools for AMD systems
|
|
5
5
|
Author-email: Mario Limonciello <superm1@kernel.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -125,8 +125,9 @@ The following optional arguments are supported for this command:
|
|
|
125
125
|
--format FORMAT Format of the report to produce (html, txt or md)
|
|
126
126
|
--report-file File to write the report to
|
|
127
127
|
--tool-debug Enable tool debug logging
|
|
128
|
-
--report-debug
|
|
129
|
-
|
|
128
|
+
--report-debug
|
|
129
|
+
--no-report-debug
|
|
130
|
+
Include debug messages in report (WARNING: can significantly increase report size)
|
|
130
131
|
If the tool is launched with an environment that can call `xdg-open`, the report
|
|
131
132
|
will be opened in a browser.
|
|
132
133
|
|
|
@@ -16,9 +16,11 @@ from platform import uname_result
|
|
|
16
16
|
from amd_debug.common import (
|
|
17
17
|
apply_prefix_wrapper,
|
|
18
18
|
Colors,
|
|
19
|
+
convert_string_to_bool,
|
|
19
20
|
colorize_choices,
|
|
20
21
|
check_lockdown,
|
|
21
22
|
compare_file,
|
|
23
|
+
find_ip_version,
|
|
22
24
|
fatal_error,
|
|
23
25
|
get_distro,
|
|
24
26
|
get_log_priority,
|
|
@@ -330,3 +332,113 @@ class TestCommon(unittest.TestCase):
|
|
|
330
332
|
default = "option1"
|
|
331
333
|
expected_output = f"{Colors.OK}{default}{Colors.ENDC}"
|
|
332
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")
|