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.

Files changed (50) hide show
  1. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/PKG-INFO +4 -3
  2. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/README.md +3 -2
  3. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/common.py +24 -0
  4. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/failures.py +13 -1
  5. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/installer.py +34 -2
  6. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/prerequisites.py +87 -24
  7. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/s2idle.py +21 -7
  8. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/sleep_report.py +2 -2
  9. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/md +0 -7
  10. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/validator.py +3 -5
  11. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/PKG-INFO +4 -3
  12. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_common.py +112 -0
  13. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_installer.py +8 -6
  14. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_prerequisites.py +194 -28
  15. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_s2idle.py +55 -20
  16. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_validator.py +3 -5
  17. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/LICENSE +0 -0
  18. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/pyproject.toml +0 -0
  19. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/setup.cfg +0 -0
  20. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/__init__.py +0 -0
  21. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/acpi.py +0 -0
  22. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/bash/amd-s2idle +0 -0
  23. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/battery.py +0 -0
  24. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/bios.py +0 -0
  25. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/database.py +0 -0
  26. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/display.py +0 -0
  27. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/kernel.py +0 -0
  28. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/pstate.py +0 -0
  29. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/s2idle-hook +0 -0
  30. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/html +0 -0
  31. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/stdout +0 -0
  32. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/templates/txt +0 -0
  33. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug/wake.py +0 -0
  34. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/SOURCES.txt +0 -0
  35. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/dependency_links.txt +0 -0
  36. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/entry_points.txt +0 -0
  37. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/requires.txt +0 -0
  38. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/amd_debug_tools.egg-info/top_level.txt +0 -0
  39. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/launcher.py +0 -0
  40. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_acpi.py +0 -0
  41. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_batteries.py +0 -0
  42. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_bios.py +0 -0
  43. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_database.py +0 -0
  44. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_display.py +0 -0
  45. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_failures.py +0 -0
  46. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_kernel.py +0 -0
  47. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_launcher.py +0 -0
  48. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_pstate.py +0 -0
  49. {amd_debug_tools-0.2.1 → amd_debug_tools-0.2.2}/src/test_sleep_report.py +0 -0
  50. {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.1
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 Include debug messages in the report
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 Include debug messages in the report
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
- return False
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
- try:
138
- cmd = ["edid-decode", p]
139
- output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode(
140
- "utf-8"
141
- )
142
- except FileNotFoundError:
143
- self.db.record_prereq(
144
- "edid-decode not installed, unable to decode EDID", "👀"
145
- )
146
- return True
147
- except subprocess.CalledProcessError as e:
148
- self.db.record_prereq(f"Failed to capture EDID table: {e.output}", "👀")
149
- return False
150
- self.db.record_debug(apply_prefix_wrapper(f"EDID for {name}:", output))
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
- output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode(
702
- "utf-8"
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
- for dev in self.pyudev.list_devices(
1103
- subsystem="thunderbolt", DEVTYPE="thunderbolt_domain"
1104
- ):
1105
- p = os.path.join(dev.sys_path, "iommu_dma_protection")
1106
- v = int(read_file(p))
1107
- debug_str += f"\t{p}: {v}\n"
1108
- found_dmar = v == 1
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
- return [since, until, get_report_file(fname, fmt), fmt]
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(since, until, fname, fmt)
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=True,
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="store_true",
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 and markdown formats
436
- if len(self.df.index) > 1 and (self.format == "html" or self.format == "md"):
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
 
@@ -28,12 +28,5 @@ Measured {{ prereq_date }}.
28
28
  ## Summary
29
29
  {{ summary }}
30
30
 
31
- {% if battery_svg %}
32
- {{ battery_svg | safe }}
33
- {% endif %}
34
- {% if hwsleep_svg %}
35
- {{ hwsleep_svg | safe }}
36
- {% endif %}
37
-
38
31
  ## Failures reported
39
32
  {{ failures }}
@@ -469,12 +469,10 @@ class SleepValidator(AmdTool):
469
469
  )
470
470
  return False
471
471
  except FileNotFoundError:
472
- self.db.record_cycle_data("HW sleep statistics file missing", "❌")
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.1
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 Include debug messages in the report
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")