amd-debug-tools 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
amd_debug/common.py CHANGED
@@ -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:
amd_debug/failures.py CHANGED
@@ -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
+ )
amd_debug/installer.py CHANGED
@@ -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 += [
amd_debug/s2idle.py CHANGED
@@ -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
 
amd_debug/sleep_report.py CHANGED
@@ -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
 
amd_debug/templates/md CHANGED
@@ -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 }}
amd_debug/validator.py CHANGED
@@ -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
 
@@ -2,44 +2,44 @@ launcher.py,sha256=_Gs0W8tUB2wkTy5Nz4qEzG0VqQcnO7xuIQj0GwV_KbY,968
2
2
  test_acpi.py,sha256=wtS43Rz95h7YEEJBeFa6Mswaeo4syBZrw4hY8i0YbJY,3117
3
3
  test_batteries.py,sha256=nN5pfP5El7Whypq3HHEpW8bufdf5EWSTVGbayfNQYP4,3360
4
4
  test_bios.py,sha256=GBAXE_rXd2G-JE0XJ8AvYcF9Me6LTyQQQ8h0Ib3cpxQ,8981
5
- test_common.py,sha256=VBPRNLt3y-oSVXC-1HGbZF7jHA_6msW-9MMmbMjNrew,11713
5
+ test_common.py,sha256=fb16Oilh5ga6VgF-UgBj6azoYzZnPrS7KpECQ3nCwlg,16335
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
- test_installer.py,sha256=LqTAjcRIc7DkdHBYBMsRsbDeVtIh_2oEVhETCb_B0cY,10183
9
+ test_installer.py,sha256=oDMCvaKqqAWjTggltacnasQ-s1gyUvXPDcNrCUGnux4,10216
10
10
  test_kernel.py,sha256=RW-eLbae02Bhwfu1cegqA1pTj6AS5IqD5lLe-6T0Rjo,7871
11
11
  test_launcher.py,sha256=govYHL0Cpj9d5msteV5SfR7Covft31rJuzRkDeytHcY,1461
12
- test_prerequisites.py,sha256=owO-tMgq_3W4Rm85TgdOFGOabKk9OrUUbRHXD0NHiPg,77018
12
+ test_prerequisites.py,sha256=4Ltxx7vGU91b9MX1AtdPLS8iyWJBCNuEKCuO0nnEb_g,83381
13
13
  test_pstate.py,sha256=a9oAJ9-LANX32XNQhplz6Y75VNYc__QqoSBKIrwvANg,6058
14
- test_s2idle.py,sha256=VT5iNNVxaL6JdupYPHTIsIVWLBgHRHGTPyXFMO_k1w0,31312
14
+ test_s2idle.py,sha256=-S3yymjprf3_LIgaPGWgzN9FmnyQG55dNc4xqWAF8Z8,32344
15
15
  test_sleep_report.py,sha256=R3cUPPT9r9q4q93xk6NFvi4ySgT5laqidk2SASuTWIo,5878
16
- test_validator.py,sha256=0dwpWFMHHG16vYo6ON_bUa5Av2NTVEJML93oAZZgcH8,30505
16
+ test_validator.py,sha256=NeiX8kT5DiHiiB5lRSJkjIpV32UzaIrW5ljKLmFtBMk,30490
17
17
  test_wake.py,sha256=6zi5GVFHQKU1sTWw3O5-aGriB9uu5713QLn4l2wjhpM,7152
18
18
  amd_debug/__init__.py,sha256=aOtpIEKGLUStrh0e4qgilHW7HgF4Od-r9pOoZ87NwAM,1105
19
19
  amd_debug/acpi.py,sha256=fkD3Sov8cRT5ryPlakRlT7Z9jiCLT9x_MPWxt3xU_tc,3161
20
20
  amd_debug/battery.py,sha256=WN-6ys9PHCZIwg7PdwyBOa62GjBp8WKG0v1YZt5_W5s,3122
21
21
  amd_debug/bios.py,sha256=wmPKDsTZeQqsHjWpv-YHdgRNlCtFdzHQ6jJf0H3hjN8,3971
22
- amd_debug/common.py,sha256=QP3UnOrMFO7NGBgi2CGUvBmtYIalm_Z9xxlvcpze_uQ,9613
22
+ amd_debug/common.py,sha256=H9tIRlRFOMwe0d3f2-vXQeK2rJl5Z1WJzkpQM9ivpOc,10347
23
23
  amd_debug/database.py,sha256=GkRg3cmaNceyQ2_hy0MBAlMbnTDPHo2co2o4ObWpnQg,10621
24
24
  amd_debug/display.py,sha256=5L9x9tI_UoulHpIvuxuVASRtdXta7UCW_JjTb5StEB0,953
25
- amd_debug/failures.py,sha256=QV3wxl9NYxUV5e0VmMy-pNLg4PoLeCVy0RvBux1pnZM,22536
26
- amd_debug/installer.py,sha256=2upWqR3e9flu-KruDXkoqCu-P4e3R7aVpMQ6dRRurTU,13064
25
+ amd_debug/failures.py,sha256=Otv3YDu7Je4ljSifVmvjObGoOY4OvLIY20pw-v4Dqkw,22911
26
+ amd_debug/installer.py,sha256=r6r_nVWv8qYdrqAvnAzQhRiS5unBDOkXsqUfHvFK8uM,14249
27
27
  amd_debug/kernel.py,sha256=xzAy-sDY5-sd4jxyU7EaBokS7YsvEjoWRuexaTJNRBc,11851
28
- amd_debug/prerequisites.py,sha256=szi1YT-iY4F3EoG7gRcN9kVTV4XsjDDMTjLfN7vkhJ4,47685
28
+ amd_debug/prerequisites.py,sha256=bKJA9ztyapB8rxrNEgc-hxazw5Uh-sP5X0S7zplGA0c,50413
29
29
  amd_debug/pstate.py,sha256=akGdJkIxBp0bx3AeGv6ictNxwv8m0j9vQ2IZB0Jx3dM,9518
30
30
  amd_debug/s2idle-hook,sha256=LLiaqPtGd0qetu9n6EYxKHZaIdHpVQDONdOuSc0pfFg,1695
31
- amd_debug/s2idle.py,sha256=QAQ1EIhD823_dBsAmSqZuuGTF--bIz2CK2xA13PgJ8c,12695
32
- amd_debug/sleep_report.py,sha256=dRoE21nkPMFoa5L9i5XrzPug4KesLfAf1RpPFB7Xpt0,15555
33
- amd_debug/validator.py,sha256=VomxJOp6ZYBp3oYEaNsD-rvio_b346VSRz7-hyhCS_c,33234
31
+ amd_debug/s2idle.py,sha256=lzxLYZBcQllyqEMZfxYEUvQO3ArgVzwL5FHymzvZvSs,13281
32
+ amd_debug/sleep_report.py,sha256=zgwcmSk7S8GAmPtPZJGP29Mr5bcWUBxwNL8HBubKs6Q,15516
33
+ amd_debug/validator.py,sha256=fgG3D0k6DS9ArgzK1SuPUds1FNGoKf2asPUwWoyCsXE,33205
34
34
  amd_debug/wake.py,sha256=xT8WrFrN6voCmXWo5dsn4mQ7iR2QJxHrrYBd3EREG-Q,3936
35
35
  amd_debug/bash/amd-s2idle,sha256=g_cle1ElCJpwE4wcLezL6y-BdasDKTnNMhrtzKLE9ks,1142
36
36
  amd_debug/templates/html,sha256=tnpqHDZF5FfhC6YNRUfOG6Vn9ZtISFr10kEXSB476Mw,14518
37
- amd_debug/templates/md,sha256=F0xt7m-lOsSz1VeucHA6a-1gsOH7rrik15biXnDgd54,904
37
+ amd_debug/templates/md,sha256=r8X2aehnH2gzj0WHYTZ5K9wAqC5y39i_3nkDORSC0uM,787
38
38
  amd_debug/templates/stdout,sha256=hyoOJ96K2dJfnWRWhyCuariLKbEHXvs9mstV_g5aMdI,469
39
39
  amd_debug/templates/txt,sha256=nNdsvbPFOhGdL7VA-_4k5aN3nB-6ouGQt6AsWst7T3w,649
40
- amd_debug_tools-0.2.1.dist-info/licenses/LICENSE,sha256=RBlZI6r3MRGzymI2VDX2iW__D2APDbMhu_Xg5t6BWeo,1066
41
- amd_debug_tools-0.2.1.dist-info/METADATA,sha256=mNzq0JUgGUeSwdF_H6BRgFb_dNFMvKgu-Htu381x0tE,6783
42
- amd_debug_tools-0.2.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
43
- amd_debug_tools-0.2.1.dist-info/entry_points.txt,sha256=HC11T2up0pPfroAn6Pg5M2jOZXhkWIipToJ1YPTKqu8,116
44
- amd_debug_tools-0.2.1.dist-info/top_level.txt,sha256=XYjxExbUTEtiIlag_5iQvZSVOC1EIxhKM4NLklReQ0k,234
45
- amd_debug_tools-0.2.1.dist-info/RECORD,,
40
+ amd_debug_tools-0.2.2.dist-info/licenses/LICENSE,sha256=RBlZI6r3MRGzymI2VDX2iW__D2APDbMhu_Xg5t6BWeo,1066
41
+ amd_debug_tools-0.2.2.dist-info/METADATA,sha256=oZ5H5dL216bbAqtOF8VmvtqkqctPIf5iPibPCjjc1tQ,6877
42
+ amd_debug_tools-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
+ amd_debug_tools-0.2.2.dist-info/entry_points.txt,sha256=HC11T2up0pPfroAn6Pg5M2jOZXhkWIipToJ1YPTKqu8,116
44
+ amd_debug_tools-0.2.2.dist-info/top_level.txt,sha256=XYjxExbUTEtiIlag_5iQvZSVOC1EIxhKM4NLklReQ0k,234
45
+ amd_debug_tools-0.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
test_common.py CHANGED
@@ -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")
test_installer.py CHANGED
@@ -192,7 +192,7 @@ class TestInstaller(unittest.TestCase):
192
192
  """Test install requirements function"""
193
193
  self.installer.set_requirements("iasl", "ethtool")
194
194
  ret = self.installer.install_dependencies()
195
- self.assertFalse(ret)
195
+ self.assertTrue(ret)
196
196
 
197
197
  @patch("builtins.print")
198
198
  @patch("amd_debug.installer.get_distro", return_value="ubuntu")
@@ -205,7 +205,9 @@ class TestInstaller(unittest.TestCase):
205
205
  """Test install requirements function for edid-decode on Ubuntu"""
206
206
  self.installer.set_requirements("edid-decode")
207
207
  ret = self.installer.install_dependencies()
208
- _mock_check_call.assert_called_once_with(["apt", "install", "edid-decode"])
208
+ _mock_check_call.assert_called_once_with(
209
+ ["apt", "install", "libdisplay-info-bin"]
210
+ )
209
211
  self.assertTrue(ret)
210
212
 
211
213
  @patch("builtins.print")
@@ -229,7 +231,7 @@ class TestInstaller(unittest.TestCase):
229
231
  self.installer.set_requirements("edid-decode")
230
232
  ret = self.installer.install_dependencies()
231
233
  _mock_check_call.assert_called_once_with(
232
- ["dnf", "install", "-y", "edid-decode"]
234
+ ["dnf", "install", "-y", "libdisplay-info"]
233
235
  )
234
236
  self.assertTrue(ret)
235
237
 
@@ -249,8 +251,8 @@ class TestInstaller(unittest.TestCase):
249
251
  """Test install requirements function for edid-decode on Arch"""
250
252
  self.installer.set_requirements("edid-decode")
251
253
  ret = self.installer.install_dependencies()
252
- _mock_check_call.assert_not_called() # edid-decode is not supported on Arch
253
- self.assertFalse(ret)
254
+ _mock_check_call.assert_called_once_with(["pacman", "-Sy", "libdisplay-info"])
255
+ self.assertTrue(ret)
254
256
 
255
257
  @patch("builtins.print")
256
258
  @patch("os.path.exists", return_value=False)
@@ -263,7 +265,7 @@ class TestInstaller(unittest.TestCase):
263
265
  """Test install requirements function for edid-decode on unsupported distro"""
264
266
  self.installer.set_requirements("edid-decode")
265
267
  ret = self.installer.install_dependencies()
266
- self.assertFalse(ret)
268
+ self.assertTrue(ret)
267
269
 
268
270
  @patch("builtins.print")
269
271
  @patch("os.path.exists", return_value=False)
test_prerequisites.py CHANGED
@@ -8,7 +8,7 @@ This module contains unit tests for the prerequisite functions in the amd-debug-
8
8
  import logging
9
9
  import unittest
10
10
  import subprocess
11
- from unittest.mock import patch, MagicMock
11
+ from unittest.mock import patch, MagicMock, mock_open
12
12
 
13
13
  from amd_debug.prerequisites import PrerequisiteValidator
14
14
  from amd_debug.failures import *
@@ -180,20 +180,22 @@ class TestPrerequisiteValidator(unittest.TestCase):
180
180
  self.assertTrue(result)
181
181
  self.mock_db.record_prereq.assert_called_with("IOMMU disabled", "✅")
182
182
 
183
- def test_check_iommu_no_dma_protection(self):
183
+ @patch(
184
+ "amd_debug.prerequisites.open",
185
+ new_callable=unittest.mock.mock_open,
186
+ read_data=b"\x00" * 45,
187
+ )
188
+ def test_check_iommu_no_dma_protection(self, _mock_open):
184
189
  """Test check_iommu when DMA protection is not enabled"""
185
190
  self.validator.cpu_family = 0x1A
186
191
  self.validator.cpu_model = 0x20
187
192
  iommu_device = MagicMock(sys_path="/sys/devices/iommu")
188
- thunderbolt_device = MagicMock(sys_path="/sys/devices/thunderbolt")
189
193
  self.mock_pyudev.list_devices.side_effect = [
190
194
  [iommu_device],
191
- [thunderbolt_device],
192
195
  [],
193
196
  [],
194
197
  ]
195
- with patch("amd_debug.prerequisites.read_file", return_value="0"):
196
- result = self.validator.check_iommu()
198
+ result = self.validator.check_iommu()
197
199
  self.assertFalse(result)
198
200
  self.assertTrue(
199
201
  any(isinstance(f, DMArNotEnabled) for f in self.validator.failures)
@@ -202,20 +204,22 @@ class TestPrerequisiteValidator(unittest.TestCase):
202
204
  "IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌"
203
205
  )
204
206
 
205
- def test_check_iommu_missing_acpi_device(self):
207
+ @patch(
208
+ "amd_debug.prerequisites.open",
209
+ new_callable=unittest.mock.mock_open,
210
+ read_data=b"\x00" * 36 + b"\xff" * 4,
211
+ )
212
+ def test_check_iommu_missing_acpi_device(self, _mock_open):
206
213
  """Test check_iommu when MSFT0201 ACPI device is missing"""
207
214
  self.validator.cpu_family = 0x1A
208
215
  self.validator.cpu_model = 0x20
209
216
  iommu_device = MagicMock(sys_path="/sys/devices/iommu")
210
- thunderbolt_device = MagicMock(sys_path="/sys/devices/thunderbolt")
211
217
  self.mock_pyudev.list_devices.side_effect = [
212
218
  [iommu_device],
213
- [thunderbolt_device],
214
219
  [],
215
220
  [],
216
221
  ]
217
- with patch("amd_debug.prerequisites.read_file", return_value="1"):
218
- result = self.validator.check_iommu()
222
+ result = self.validator.check_iommu()
219
223
  self.assertFalse(result)
220
224
  self.assertTrue(
221
225
  any(isinstance(f, MissingIommuACPI) for f in self.validator.failures)
@@ -224,47 +228,48 @@ class TestPrerequisiteValidator(unittest.TestCase):
224
228
  "IOMMU is misconfigured: missing MSFT0201 ACPI device", "❌"
225
229
  )
226
230
 
227
- def test_check_iommu_missing_policy(self):
231
+ @patch(
232
+ "amd_debug.prerequisites.open",
233
+ new_callable=unittest.mock.mock_open,
234
+ read_data=b"\x00" * 36 + b"\xff" * 4,
235
+ )
236
+ def test_check_iommu_missing_policy(self, _mock_open):
228
237
  """Test check_iommu when policy is not bound to MSFT0201"""
229
238
  self.validator.cpu_family = 0x1A
230
239
  self.validator.cpu_model = 0x20
231
240
  iommu_device = MagicMock(sys_path="/sys/devices/iommu")
232
- thunderbolt_device = MagicMock(sys_path="/sys/devices/thunderbolt")
233
241
  acpi_device = MagicMock(sys_path="/sys/devices/acpi/MSFT0201")
234
242
  platform_device = MagicMock(sys_path="/sys/devices/platform/MSFT0201")
235
243
  self.mock_pyudev.list_devices.side_effect = [
236
244
  [iommu_device],
237
- [thunderbolt_device],
238
245
  [acpi_device],
239
246
  [platform_device],
240
247
  ]
241
- with patch("amd_debug.prerequisites.read_file", return_value="1"), patch(
242
- "os.path.exists", return_value=False
243
- ):
244
- result = self.validator.check_iommu()
248
+ result = self.validator.check_iommu()
245
249
  self.assertFalse(result)
246
250
  self.assertTrue(
247
251
  any(isinstance(f, MissingIommuPolicy) for f in self.validator.failures)
248
252
  )
249
253
 
250
- def test_check_iommu_properly_configured(self):
254
+ @patch(
255
+ "amd_debug.prerequisites.open",
256
+ new_callable=unittest.mock.mock_open,
257
+ read_data=b"\x00" * 36 + b"\xff" * 4,
258
+ )
259
+ @patch("amd_debug.prerequisites.os.path.exists", return_value=True)
260
+ def test_check_iommu_properly_configured(self, _mock_open, _mock_exists):
251
261
  """Test check_iommu when IOMMU is properly configured"""
252
262
  self.validator.cpu_family = 0x1A
253
263
  self.validator.cpu_model = 0x20
254
264
  iommu_device = MagicMock(sys_path="/sys/devices/iommu")
255
- thunderbolt_device = MagicMock(sys_path="/sys/devices/thunderbolt")
256
265
  acpi_device = MagicMock(sys_path="/sys/devices/acpi/MSFT0201")
257
266
  platform_device = MagicMock(sys_path="/sys/devices/platform/MSFT0201")
258
267
  self.mock_pyudev.list_devices.side_effect = [
259
268
  [iommu_device],
260
- [thunderbolt_device],
261
269
  [acpi_device],
262
270
  [platform_device],
263
271
  ]
264
- with patch("amd_debug.prerequisites.read_file", return_value="1"), patch(
265
- "os.path.exists", return_value=True
266
- ):
267
- result = self.validator.check_iommu()
272
+ result = self.validator.check_iommu()
268
273
  self.assertTrue(result)
269
274
  self.mock_db.record_prereq.assert_called_with("IOMMU properly configured", "✅")
270
275
 
@@ -1737,7 +1742,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
1737
1742
  result = self.validator.capture_edid()
1738
1743
  self.assertTrue(result)
1739
1744
  self.mock_db.record_prereq.assert_called_with(
1740
- "edid-decode not installed, unable to decode EDID", "👀"
1745
+ "Failed to capture EDID table", "👀"
1741
1746
  )
1742
1747
 
1743
1748
  @patch("amd_debug.prerequisites.subprocess.check_output")
@@ -1750,9 +1755,9 @@ class TestPrerequisiteValidator(unittest.TestCase):
1750
1755
  returncode=1, cmd="edid-decode", output=b"Error decoding EDID"
1751
1756
  )
1752
1757
  result = self.validator.capture_edid()
1753
- self.assertFalse(result)
1758
+ self.assertTrue(result)
1754
1759
  self.mock_db.record_prereq.assert_called_with(
1755
- "Failed to capture EDID table: b'Error decoding EDID'", "👀"
1760
+ "Failed to capture EDID table", "👀"
1756
1761
  )
1757
1762
 
1758
1763
  @patch("amd_debug.prerequisites.subprocess.check_output")
@@ -1767,3 +1772,164 @@ class TestPrerequisiteValidator(unittest.TestCase):
1767
1772
  self.mock_db.record_debug.assert_called_with(
1768
1773
  apply_prefix_wrapper("EDID for Monitor1:", "Decoded EDID data")
1769
1774
  )
1775
+
1776
+ @patch("amd_debug.prerequisites.find_ip_version", return_value=True)
1777
+ @patch("amd_debug.prerequisites.os.path.exists")
1778
+ @patch("amd_debug.prerequisites.read_file")
1779
+ def test_check_dpia_pg_dmcub_usb4_found(
1780
+ self, mock_read_file, mock_path_exists, mock_find_ip_version
1781
+ ):
1782
+ """Test check_dpia_pg_dmcub when USB4 routers are found"""
1783
+ usb4_device = MagicMock()
1784
+ self.mock_pyudev.list_devices.side_effect = [
1785
+ [usb4_device], # First call: USB4 present
1786
+ ]
1787
+ result = self.validator.check_dpia_pg_dmcub()
1788
+ self.assertTrue(result)
1789
+ self.mock_db.record_debug.assert_called_with(
1790
+ "USB4 routers found, no need to check DMCUB version"
1791
+ )
1792
+
1793
+ @patch("amd_debug.prerequisites.find_ip_version", return_value=True)
1794
+ @patch("amd_debug.prerequisites.os.path.exists", return_value=True)
1795
+ @patch("amd_debug.prerequisites.read_file", return_value="0x90001B01")
1796
+ def test_check_dpia_pg_dmcub_dmcub_fw_version_new_enough(
1797
+ self, mock_read_file, mock_path_exists, mock_find_ip_version
1798
+ ):
1799
+ """Test check_dpia_pg_dmcub when DMCUB firmware version is new enough"""
1800
+ self.mock_pyudev.list_devices.side_effect = [
1801
+ [], # First call: no USB4
1802
+ [
1803
+ MagicMock(
1804
+ properties={
1805
+ "PCI_CLASS": "30000",
1806
+ "PCI_ID": "1002abcd",
1807
+ "PCI_SLOT_NAME": "0000:01:00.0",
1808
+ },
1809
+ sys_path="/sys/devices/pci0000:01/0000:01:00.0",
1810
+ )
1811
+ ],
1812
+ ]
1813
+ with patch("builtins.open", new_callable=mock_open, read_data="3") as mock_file:
1814
+ handlers = (
1815
+ mock_file.return_value,
1816
+ mock_open(read_data="5").return_value,
1817
+ mock_open(read_data="0").return_value,
1818
+ )
1819
+ mock_open.side_effect = handlers
1820
+ result = self.validator.check_dpia_pg_dmcub()
1821
+ self.assertTrue(result)
1822
+ self.mock_db.record_prereq.assert_not_called()
1823
+
1824
+ @patch("amd_debug.prerequisites.find_ip_version", return_value=True)
1825
+ @patch("amd_debug.prerequisites.os.path.exists", return_value=True)
1826
+ @patch("amd_debug.prerequisites.read_file", return_value="0x8001B00")
1827
+ def test_check_dpia_pg_dmcub_dmcub_fw_version_too_old(
1828
+ self, mock_read_file, mock_path_exists, mock_find_ip_version
1829
+ ):
1830
+ """Test check_dpia_pg_dmcub when DMCUB firmware version is too old"""
1831
+ self.mock_pyudev.list_devices.side_effect = [
1832
+ [], # First call: no USB4
1833
+ [
1834
+ MagicMock(
1835
+ properties={
1836
+ "PCI_CLASS": "30000",
1837
+ "PCI_ID": "1002abcd",
1838
+ "PCI_SLOT_NAME": "0000:01:00.0",
1839
+ },
1840
+ sys_path="/sys/devices/pci0000:01/0000:01:00.0",
1841
+ )
1842
+ ],
1843
+ ]
1844
+ result = self.validator.check_dpia_pg_dmcub()
1845
+ self.assertFalse(result)
1846
+ self.mock_db.record_prereq.assert_called_with(
1847
+ "DMCUB Firmware is outdated", "❌"
1848
+ )
1849
+ self.assertTrue(
1850
+ any(isinstance(f, DmcubTooOld) for f in self.validator.failures)
1851
+ )
1852
+
1853
+ @patch("amd_debug.prerequisites.find_ip_version", return_value=True)
1854
+ @patch("amd_debug.prerequisites.os.path.exists", return_value=False)
1855
+ @patch(
1856
+ "amd_debug.prerequisites.read_file",
1857
+ side_effect=[
1858
+ "", # sysfs read returns empty, so fallback to debugfs
1859
+ "DMCUB fw: 09001B00\nOther line\n", # debugfs read
1860
+ ],
1861
+ )
1862
+ def test_check_dpia_pg_dmcub_debugfs_version_new_enough(
1863
+ self, mock_read_file, mock_path_exists, mock_find_ip_version
1864
+ ):
1865
+ """Test check_dpia_pg_dmcub when DMCUB version is found in debugfs and is new enough"""
1866
+ self.mock_pyudev.list_devices.side_effect = [
1867
+ [], # First call: no USB4
1868
+ [
1869
+ MagicMock(
1870
+ properties={
1871
+ "PCI_CLASS": "30000",
1872
+ "PCI_ID": "1002abcd",
1873
+ "PCI_SLOT_NAME": "0",
1874
+ },
1875
+ sys_path="/sys/devices/pci0000:01/0000:01:00.0",
1876
+ )
1877
+ ],
1878
+ ]
1879
+ result = self.validator.check_dpia_pg_dmcub()
1880
+ self.assertTrue(result)
1881
+ self.mock_db.record_prereq.assert_not_called()
1882
+
1883
+ @patch("amd_debug.prerequisites.find_ip_version", return_value=True)
1884
+ @patch("amd_debug.prerequisites.os.path.exists", return_value=False)
1885
+ @patch(
1886
+ "amd_debug.prerequisites.read_file",
1887
+ side_effect=[
1888
+ "DMCUB fw: 0x08001B00\nOther line\n", # debugfs read
1889
+ ],
1890
+ )
1891
+ def test_check_dpia_pg_dmcub_debugfs_version_too_old(
1892
+ self, mock_read_file, mock_path_exists, mock_find_ip_version
1893
+ ):
1894
+ """Test check_dpia_pg_dmcub when DMCUB version is found in debugfs and is too old"""
1895
+ self.mock_pyudev.list_devices.side_effect = [
1896
+ [], # First call: no USB4
1897
+ [
1898
+ MagicMock(
1899
+ properties={
1900
+ "PCI_CLASS": "30000",
1901
+ "PCI_ID": "1002abcd",
1902
+ "PCI_SLOT_NAME": "0",
1903
+ },
1904
+ sys_path="/sys/devices/pci0000:01/0000:01:00.0",
1905
+ )
1906
+ ],
1907
+ ]
1908
+ result = self.validator.check_dpia_pg_dmcub()
1909
+ self.assertFalse(result)
1910
+ self.mock_db.record_prereq.assert_called_with(
1911
+ "DMCUB Firmware is outdated", "❌"
1912
+ )
1913
+ self.assertTrue(
1914
+ any(isinstance(f, DmcubTooOld) for f in self.validator.failures)
1915
+ )
1916
+
1917
+ @patch("amd_debug.prerequisites.find_ip_version", return_value=False)
1918
+ def test_check_dpia_pg_dmcub_no_matching_dcn(self, mock_find_ip_version):
1919
+ """Test check_dpia_pg_dmcub when no matching DCN is found"""
1920
+ self.mock_pyudev.list_devices.side_effect = [
1921
+ [], # First call: no USB4
1922
+ [
1923
+ MagicMock(
1924
+ properties={
1925
+ "PCI_CLASS": "30000",
1926
+ "PCI_ID": "1002abcd",
1927
+ "PCI_SLOT_NAME": "0",
1928
+ },
1929
+ sys_path="/sys/devices/pci0000:01/0000:01:00.0",
1930
+ )
1931
+ ],
1932
+ ]
1933
+ result = self.validator.check_dpia_pg_dmcub()
1934
+ self.assertTrue(result)
1935
+ self.mock_db.record_prereq.assert_not_called()
test_s2idle.py CHANGED
@@ -381,6 +381,7 @@ class TestTestFunction(unittest.TestCase):
381
381
  "2023-02-01",
382
382
  "report.html",
383
383
  "html",
384
+ True,
384
385
  )
385
386
 
386
387
  mock_sleep_validator_instance = mock_sleep_validator.return_value
@@ -605,6 +606,7 @@ class TestReportFunction(unittest.TestCase):
605
606
  "2023-02-01",
606
607
  "report.html",
607
608
  "html",
609
+ True,
608
610
  )
609
611
  mock_sleep_report_instance = mock_sleep_report.return_value
610
612
 
@@ -614,10 +616,12 @@ class TestReportFunction(unittest.TestCase):
614
616
  fname=None,
615
617
  fmt=None,
616
618
  tool_debug=True,
617
- report_debug=True,
619
+ report_debug=None,
618
620
  )
619
621
 
620
- mock_prompt_report_arguments.assert_called_once_with(None, None, None, None)
622
+ mock_prompt_report_arguments.assert_called_once_with(
623
+ None, None, None, None, None
624
+ )
621
625
  mock_sleep_report.assert_called_once_with(
622
626
  since="2023-01-01",
623
627
  until="2023-02-01",
@@ -640,10 +644,12 @@ class TestReportFunction(unittest.TestCase):
640
644
  fname=None,
641
645
  fmt=None,
642
646
  tool_debug=True,
643
- report_debug=True,
647
+ report_debug=None,
644
648
  )
645
649
 
646
- mock_prompt_report_arguments.assert_called_once_with(None, None, None, None)
650
+ mock_prompt_report_arguments.assert_called_once_with(
651
+ None, None, None, None, None
652
+ )
647
653
 
648
654
  @patch("amd_debug.s2idle.prompt_report_arguments")
649
655
  @patch(
@@ -659,6 +665,7 @@ class TestReportFunction(unittest.TestCase):
659
665
  "2023-02-01",
660
666
  "report.html",
661
667
  "html",
668
+ True,
662
669
  )
663
670
 
664
671
  result = report(
@@ -667,10 +674,12 @@ class TestReportFunction(unittest.TestCase):
667
674
  fname=None,
668
675
  fmt=None,
669
676
  tool_debug=True,
670
- report_debug=True,
677
+ report_debug=None,
671
678
  )
672
679
 
673
- mock_prompt_report_arguments.assert_called_once_with(None, None, None, None)
680
+ mock_prompt_report_arguments.assert_called_once_with(
681
+ None, None, None, None, None
682
+ )
674
683
  mock_sleep_report.assert_called_once_with(
675
684
  since="2023-01-01",
676
685
  until="2023-02-01",
@@ -695,6 +704,7 @@ class TestReportFunction(unittest.TestCase):
695
704
  "2023-02-01",
696
705
  "report.html",
697
706
  "html",
707
+ True,
698
708
  )
699
709
 
700
710
  result = report(
@@ -703,10 +713,12 @@ class TestReportFunction(unittest.TestCase):
703
713
  fname=None,
704
714
  fmt=None,
705
715
  tool_debug=True,
706
- report_debug=True,
716
+ report_debug=None,
707
717
  )
708
718
 
709
- mock_prompt_report_arguments.assert_called_once_with(None, None, None, None)
719
+ mock_prompt_report_arguments.assert_called_once_with(
720
+ None, None, None, None, None
721
+ )
710
722
  mock_sleep_report.assert_called_once_with(
711
723
  since="2023-01-01",
712
724
  until="2023-02-01",
@@ -729,6 +741,7 @@ class TestReportFunction(unittest.TestCase):
729
741
  "2023-02-01",
730
742
  "report.html",
731
743
  "html",
744
+ True,
732
745
  )
733
746
  mock_sleep_report_instance = mock_sleep_report.return_value
734
747
  mock_sleep_report_instance.run.side_effect = ValueError("Invalid value")
@@ -739,10 +752,12 @@ class TestReportFunction(unittest.TestCase):
739
752
  fname=None,
740
753
  fmt=None,
741
754
  tool_debug=True,
742
- report_debug=True,
755
+ report_debug=None,
743
756
  )
744
757
 
745
- mock_prompt_report_arguments.assert_called_once_with(None, None, None, None)
758
+ mock_prompt_report_arguments.assert_called_once_with(
759
+ None, None, None, None, None
760
+ )
746
761
  mock_sleep_report.assert_called_once_with(
747
762
  since="2023-01-01",
748
763
  until="2023-02-01",
@@ -758,14 +773,14 @@ class TestReportFunction(unittest.TestCase):
758
773
  class TestPromptReportArguments(unittest.TestCase):
759
774
  """Test prompt_report_arguments function"""
760
775
 
761
- @patch("builtins.input", side_effect=["2023-01-01", "2023-02-01", "html"])
776
+ @patch("builtins.input", side_effect=["2023-01-01", "2023-02-01", "html", "true"])
762
777
  @patch("amd_debug.s2idle.get_report_file", return_value="report.html")
763
778
  @patch("amd_debug.s2idle.get_report_format", return_value="html")
764
779
  def test_prompt_report_arguments_success(
765
780
  self, mock_get_report_format, mock_get_report_file, _mock_input
766
781
  ):
767
782
  """Test prompt_report_arguments with valid inputs"""
768
- result = prompt_report_arguments(None, None, None, None)
783
+ result = prompt_report_arguments(None, None, None, None, None)
769
784
  self.assertEqual(result[0], datetime(2023, 1, 1))
770
785
  self.assertEqual(result[1], datetime(2023, 2, 1))
771
786
  self.assertEqual(result[2], "report.html")
@@ -775,33 +790,36 @@ class TestPromptReportArguments(unittest.TestCase):
775
790
 
776
791
  @patch(
777
792
  "builtins.input",
778
- side_effect=["invalid-date", "2023-01-01", "2023-02-01", "html"],
793
+ side_effect=["invalid-date", "2023-01-01", "2023-02-01", "html", "true"],
779
794
  )
780
795
  @patch("sys.exit")
781
796
  def test_prompt_report_arguments_invalid_since_date(self, mock_exit, mock_input):
782
797
  """Test prompt_report_arguments with invalid 'since' date"""
783
798
  mock_exit.side_effect = SystemExit
784
799
  with self.assertRaises(SystemExit):
785
- prompt_report_arguments(None, None, None, None)
800
+ prompt_report_arguments(None, None, None, None, None)
786
801
  mock_exit.assert_called_once_with(
787
802
  "Invalid date, use YYYY-MM-DD: Invalid isoformat string: 'invalid-date'"
788
803
  )
789
804
 
790
805
  @patch(
791
806
  "builtins.input",
792
- side_effect=["2023-01-01", "invalid-date", "2023-02-01", "html"],
807
+ side_effect=["2023-01-01", "invalid-date", "2023-02-01", "html", "true"],
793
808
  )
794
809
  @patch("sys.exit")
795
810
  def test_prompt_report_arguments_invalid_until_date(self, mock_exit, mock_input):
796
811
  """Test prompt_report_arguments with invalid 'until' date"""
797
812
  mock_exit.side_effect = SystemExit
798
813
  with self.assertRaises(SystemExit):
799
- prompt_report_arguments(None, None, None, None)
814
+ prompt_report_arguments(None, None, None, None, None)
800
815
  mock_exit.assert_called_once_with(
801
816
  "Invalid date, use YYYY-MM-DD: Invalid isoformat string: 'invalid-date'"
802
817
  )
803
818
 
804
- @patch("builtins.input", side_effect=["2023-01-01", "2023-02-01", "invalid-format"])
819
+ @patch(
820
+ "builtins.input",
821
+ side_effect=["2023-01-01", "2023-02-01", "invalid-format", "true"],
822
+ )
805
823
  @patch("amd_debug.s2idle.get_report_format", return_value="html")
806
824
  @patch("sys.exit")
807
825
  def test_prompt_report_arguments_invalid_format(
@@ -810,11 +828,27 @@ class TestPromptReportArguments(unittest.TestCase):
810
828
  """Test prompt_report_arguments with invalid format"""
811
829
  mock_exit.side_effect = SystemExit
812
830
  with self.assertRaises(SystemExit):
813
- prompt_report_arguments(None, None, None, None)
831
+ prompt_report_arguments(None, None, None, None, None)
814
832
  mock_exit.assert_called_once_with("Invalid format: invalid-format")
815
833
  mock_get_report_format.assert_called_once()
816
834
 
817
- @patch("builtins.input", side_effect=["", "", ""])
835
+ @patch(
836
+ "builtins.input",
837
+ side_effect=["2023-01-01", "2023-02-01", "html", "foo_the_bar"],
838
+ )
839
+ @patch("amd_debug.s2idle.get_report_format", return_value="html")
840
+ @patch("sys.exit")
841
+ def test_prompt_report_arguments_invalid_report(
842
+ self, mock_exit, mock_get_report_format, mock_input
843
+ ):
844
+ """Test prompt_report_arguments with invalid format"""
845
+ mock_exit.side_effect = SystemExit
846
+ with self.assertRaises(SystemExit):
847
+ prompt_report_arguments(None, None, None, None, None)
848
+ mock_exit.assert_called_once_with("Invalid entry: Foo_the_bar")
849
+ mock_get_report_format.assert_called_once()
850
+
851
+ @patch("builtins.input", side_effect=["", "", "", ""])
818
852
  @patch(
819
853
  "amd_debug.s2idle.get_report_file",
820
854
  return_value="amd-s2idle-report-2023-01-01.html",
@@ -824,10 +858,11 @@ class TestPromptReportArguments(unittest.TestCase):
824
858
  self, mock_get_report_format, mock_get_report_file, mock_input
825
859
  ):
826
860
  """Test prompt_report_arguments with default values"""
827
- result = prompt_report_arguments(None, None, None, None)
861
+ result = prompt_report_arguments(None, None, None, None, None)
828
862
  self.assertEqual(datetime.date(result[0]), Defaults.since)
829
863
  self.assertEqual(datetime.date(result[1]), Defaults.until)
830
864
  self.assertEqual(result[2], "amd-s2idle-report-2023-01-01.html")
831
865
  self.assertEqual(result[3], "html")
866
+ self.assertEqual(result[4], True)
832
867
  mock_get_report_file.assert_called_once_with(None, "html")
833
868
  mock_get_report_format.assert_called()
test_validator.py CHANGED
@@ -321,13 +321,11 @@ class TestValidator(unittest.TestCase):
321
321
  self.validator.lockdown = False
322
322
  with patch("os.path.exists", return_value=False), patch(
323
323
  "amd_debug.validator.read_file", side_effect=FileNotFoundError
324
- ), patch.object(
325
- self.validator.db, "record_cycle_data"
326
- ) as mock_record_cycle_data:
324
+ ), patch.object(self.validator.db, "record_debug") as mock_record:
327
325
  result = self.validator.capture_hw_sleep()
328
326
  self.assertFalse(result)
329
- mock_record_cycle_data.assert_called_once_with(
330
- "HW sleep statistics file missing", "❌"
327
+ mock_record.assert_called_once_with(
328
+ "HW sleep statistics file /sys/kernel/debug/amd_pmc/smu_fw_info is missing"
331
329
  )
332
330
 
333
331
  def test_capture_amdgpu_ips_status(self):