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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

amd_debug/failures.py CHANGED
@@ -142,7 +142,7 @@ class MissingDriver(S0i3Failure):
142
142
  self.description = f"{slot} driver is missing"
143
143
  self.explanation = (
144
144
  f"No driver has been bound to PCI device {slot} "
145
- "Without a driver, the hardware may be able to enter a low power. "
145
+ "Without a driver, the hardware may be able to enter a low power "
146
146
  "state, but there may be spurious wake up events."
147
147
  )
148
148
 
amd_debug/kernel.py CHANGED
@@ -19,6 +19,7 @@ def get_kernel_command_line() -> str:
19
19
  "apparmor",
20
20
  "audit",
21
21
  "auto",
22
+ "bluetooth.disable_ertm",
22
23
  "boot",
23
24
  "BOOT_IMAGE",
24
25
  "console",
@@ -29,6 +30,7 @@ def get_kernel_command_line() -> str:
29
30
  "earlycon",
30
31
  "earlyprintk",
31
32
  "ether",
33
+ "init",
32
34
  "initrd",
33
35
  "ip",
34
36
  "LANG",
@@ -47,7 +49,9 @@ def get_kernel_command_line() -> str:
47
49
  "nfs.nfs4_unique_id",
48
50
  "nfsroot",
49
51
  "noplymouth",
52
+ "nowatchdog",
50
53
  "ostree",
54
+ "preempt",
51
55
  "quiet",
52
56
  "rd.dm.uuid",
53
57
  "rd.luks.allow-discards",
@@ -66,12 +70,15 @@ def get_kernel_command_line() -> str:
66
70
  "ro",
67
71
  "root",
68
72
  "rootflags",
73
+ "rootfstype",
69
74
  "roothash",
70
75
  "rw",
71
76
  "security",
77
+ "selinux",
72
78
  "showopts",
73
79
  "splash",
74
80
  "swap",
81
+ "systemd.machine_id",
75
82
  "systemd.mask",
76
83
  "systemd.show_status",
77
84
  "systemd.unit",
@@ -82,6 +89,7 @@ def get_kernel_command_line() -> str:
82
89
  "verbose",
83
90
  "vt.handoff",
84
91
  "zfs",
92
+ "zswap.enabled",
85
93
  ]
86
94
  # remove anything that starts with something in filtered from cmdline
87
95
  return " ".join([x for x in cmdline.split() if not x.startswith(tuple(filtered))])
@@ -134,22 +134,19 @@ class PrerequisiteValidator(AmdTool):
134
134
  if len(edids) == 0:
135
135
  self.db.record_debug("No EDID data found")
136
136
  return True
137
-
138
137
  for name, p in edids.items():
139
138
  output = None
140
- for cmd in ["di-edid-decode", "edid-decode"]:
139
+ for tool in ["di-edid-decode", "edid-decode"]:
141
140
  try:
142
- cmd = ["edid-decode", p]
141
+ cmd = [tool, p]
143
142
  output = subprocess.check_output(
144
143
  cmd, stderr=subprocess.DEVNULL
145
- ).decode("utf-8")
144
+ ).decode("utf-8", errors="ignore")
146
145
  break
147
146
  except FileNotFoundError:
148
147
  self.db.record_debug(f"{cmd} not installed")
149
148
  except subprocess.CalledProcessError as e:
150
- self.db.record_debug(
151
- f"failed to capture edid with {cmd}: {e.output}"
152
- )
149
+ pass
153
150
  if not output:
154
151
  self.db.record_prereq("Failed to capture EDID table", "👀")
155
152
  else:
@@ -245,7 +242,6 @@ class PrerequisiteValidator(AmdTool):
245
242
  for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="nvme"):
246
243
  # https://git.kernel.org/torvalds/c/e79a10652bbd3
247
244
  if minimum_kernel(6, 10):
248
- self.db.record_debug("New enough kernel to avoid NVME check")
249
245
  break
250
246
  pci_slot_name = dev.properties["PCI_SLOT_NAME"]
251
247
  vendor = dev.properties.get("ID_VENDOR_FROM_DATABASE", "")
@@ -319,7 +315,6 @@ class PrerequisiteValidator(AmdTool):
319
315
  # not needed to check in newer kernels
320
316
  # see https://github.com/torvalds/linux/commit/77f1972bdcf7513293e8bbe376b9fe837310ee9c
321
317
  if minimum_kernel(6, 10):
322
- self.db.record_debug("New enough kernel to avoid HSMP check")
323
318
  return True
324
319
  f = os.path.join("/", "boot", f"config-{platform.uname().release}")
325
320
  if os.path.exists(f):
@@ -527,7 +522,7 @@ class PrerequisiteValidator(AmdTool):
527
522
  f"{keys['sys_vendor']} {keys['product_name']} ({keys['product_family']})",
528
523
  "💻",
529
524
  )
530
- debug_str = "DMI data:\n"
525
+ debug_str = "DMI|value\n"
531
526
  for key, value in keys.items():
532
527
  if (
533
528
  "product_name" in key
@@ -535,7 +530,7 @@ class PrerequisiteValidator(AmdTool):
535
530
  or "product_family" in key
536
531
  ):
537
532
  continue
538
- debug_str += f"{key}: {value}\n"
533
+ debug_str += f"{key}| {value}\n"
539
534
  self.db.record_debug(debug_str)
540
535
  return True
541
536
 
@@ -916,7 +911,7 @@ class PrerequisiteValidator(AmdTool):
916
911
  devices = []
917
912
  for dev in self.pyudev.list_devices(subsystem="pci"):
918
913
  devices.append(dev)
919
- debug_str = "PCI devices\n"
914
+ debug_str = "PCI Slot | Vendor | Class | ID | ACPI path\n"
920
915
  for dev in devices:
921
916
  pci_id = dev.properties["PCI_ID"].lower()
922
917
  pci_slot_name = dev.properties["PCI_SLOT_NAME"]
@@ -939,15 +934,12 @@ class PrerequisiteValidator(AmdTool):
939
934
  p = os.path.join(dev.sys_path, "firmware_node", "path")
940
935
  if os.path.exists(p):
941
936
  acpi = read_file(p)
942
- debug_str += (
943
- f"{prefix}{pci_slot_name} : "
944
- f"{database_vendor} {database_class} [{pci_id}] : {acpi}\n"
945
- )
946
937
  else:
947
- debug_str += (
948
- f"{prefix}{pci_slot_name} : "
949
- f"{database_vendor} {database_class} [{pci_id}]\n"
950
- )
938
+ acpi = ""
939
+ debug_str += (
940
+ f"{prefix}{pci_slot_name} | "
941
+ f"{database_vendor} | {database_class} | {pci_id} | {acpi}\n"
942
+ )
951
943
  if debug_str:
952
944
  self.db.record_debug(debug_str)
953
945
 
@@ -964,12 +956,8 @@ class PrerequisiteValidator(AmdTool):
964
956
  if status == 0:
965
957
  continue
966
958
  devices.append(dev)
967
- debug_str = "ACPI name: ACPI path [driver]\n"
959
+ debug_str = "ACPI name | ACPI path | Kernel driver\n"
968
960
  for dev in devices:
969
- if dev == devices[-1]:
970
- prefix = "└─"
971
- else:
972
- prefix = "│ "
973
961
  p = os.path.join(dev.sys_path, "path")
974
962
  pth = read_file(p)
975
963
  p = os.path.join(dev.sys_path, "physical_node", "driver")
@@ -977,7 +965,7 @@ class PrerequisiteValidator(AmdTool):
977
965
  driver = os.path.basename(os.readlink(p))
978
966
  else:
979
967
  driver = None
980
- debug_str += f"{prefix}{dev.sys_name}: {pth} [{driver}]\n"
968
+ debug_str += f"{dev.sys_name} | {pth} | {driver}\n"
981
969
  if debug_str:
982
970
  self.db.record_debug(debug_str)
983
971
  return True
@@ -1157,7 +1145,6 @@ class PrerequisiteValidator(AmdTool):
1157
1145
  if not found_iommu:
1158
1146
  self.db.record_prereq("IOMMU disabled", "✅")
1159
1147
  return True
1160
- debug_str += "DMA protection:\n"
1161
1148
  p = os.path.join("/", "sys", "firmware", "acpi", "tables", "IVRS")
1162
1149
  with open(p, "rb") as f:
1163
1150
  data = f.read()
amd_debug/s2idle.py CHANGED
@@ -211,6 +211,14 @@ def run_test_cycle(
211
211
  print("Failed to install dependencies")
212
212
  return False
213
213
 
214
+ try:
215
+ duration, wait, count = prompt_test_arguments(duration, wait, count, rand)
216
+ since, until, fname, fmt, report_debug = prompt_report_arguments(
217
+ datetime.now().isoformat(), Defaults.until.isoformat(), fname, fmt, True
218
+ )
219
+ except KeyboardInterrupt:
220
+ sys.exit("\nTest cancelled")
221
+
214
222
  try:
215
223
  app = PrerequisiteValidator(debug)
216
224
  run = app.run()
@@ -221,37 +229,32 @@ def run_test_cycle(
221
229
 
222
230
  if run or force:
223
231
  app = SleepValidator(tool_debug=debug, bios_debug=bios_debug)
224
- try:
225
- duration, wait, count = prompt_test_arguments(duration, wait, count, rand)
226
- since, until, fname, fmt, report_debug = prompt_report_arguments(
227
- datetime.now().isoformat(), Defaults.until.isoformat(), fname, fmt, True
228
- )
229
- except KeyboardInterrupt:
230
- sys.exit("\nTest cancelled")
231
232
 
232
- app.run(
233
+ run = app.run(
233
234
  duration=duration,
234
235
  wait=wait,
235
236
  count=count,
236
237
  rand=rand,
237
238
  logind=logind,
238
239
  )
240
+ else:
241
+ since = None
242
+ until = None
243
+
244
+ app = SleepReport(
245
+ since=since,
246
+ until=until,
247
+ fname=fname,
248
+ fmt=fmt,
249
+ tool_debug=debug,
250
+ report_debug=report_debug,
251
+ )
252
+ app.run()
239
253
 
240
- app = SleepReport(
241
- since=since,
242
- until=until,
243
- fname=fname,
244
- fmt=fmt,
245
- tool_debug=debug,
246
- report_debug=report_debug,
247
- )
248
- app.run()
249
-
250
- # open report in browser if it's html
251
- display_report_file(fname, fmt)
254
+ # open report in browser if it's html
255
+ display_report_file(fname, fmt)
252
256
 
253
- return True
254
- return False
257
+ return True
255
258
 
256
259
 
257
260
  def install(debug) -> None:
@@ -308,10 +311,8 @@ def parse_args():
308
311
  test_cmd.add_argument(
309
312
  "--random",
310
313
  action="store_true",
311
- help=(
312
- "Run sleep cycles for random durations and wait, using the "
313
- "--duration and --wait arguments as an upper bound",
314
- ),
314
+ help="Run sleep cycles for random durations and wait, using the "
315
+ "--duration and --wait arguments as an upper bound",
315
316
  )
316
317
  test_cmd.add_argument(
317
318
  "--force",
amd_debug/sleep_report.py CHANGED
@@ -5,6 +5,7 @@ import os
5
5
  import re
6
6
  import math
7
7
  from datetime import datetime, timedelta
8
+ import numpy as np
8
9
  from tabulate import tabulate
9
10
  from jinja2 import Environment, FileSystemLoader
10
11
  import pandas as pd
@@ -73,6 +74,8 @@ def format_percent(val):
73
74
 
74
75
  def format_timedelta(val):
75
76
  """Format seconds as a nicer format"""
77
+ if math.isnan(val):
78
+ val = 0
76
79
  return str(timedelta(seconds=val))
77
80
 
78
81
 
@@ -97,8 +100,23 @@ class SleepReport(AmdTool):
97
100
  self.debug = report_debug
98
101
  self.format = fmt
99
102
  self.failures = []
100
- self.df = self.db.report_summary_dataframe(self.since, self.until)
101
- self.pre_process_dataframe()
103
+ if since and until:
104
+ self.df = self.db.report_summary_dataframe(self.since, self.until)
105
+ self.pre_process_dataframe()
106
+ else:
107
+ self.df = pd.DataFrame(
108
+ columns=[
109
+ "t0",
110
+ "t1",
111
+ "requested",
112
+ "hw",
113
+ "b0",
114
+ "b1",
115
+ "full",
116
+ "wake_irq",
117
+ "gpio",
118
+ ]
119
+ )
102
120
  self.battery_svg = None
103
121
  self.hwsleep_svg = None
104
122
 
@@ -136,6 +154,7 @@ class SleepReport(AmdTool):
136
154
  self.df["Duration"] = self.df["t1"].apply(format_as_seconds) - self.df[
137
155
  "t0"
138
156
  ].apply(format_as_seconds)
157
+ self.df["Duration"] = self.df["Duration"].replace(0, np.nan)
139
158
  self.df["Hardware Sleep"] = (self.df["hw"] / self.df["Duration"]).apply(
140
159
  parse_hw_sleep
141
160
  )
@@ -187,7 +206,8 @@ class SleepReport(AmdTool):
187
206
  format_watts
188
207
  )
189
208
 
190
- def convert_gpio_dataframe(self, content):
209
+ def convert_table_dataframe(self, content):
210
+ """Convert a table like dataframe to an HTML table"""
191
211
  header = False
192
212
  rows = []
193
213
  for line in content.split("\n"):
@@ -196,17 +216,25 @@ class SleepReport(AmdTool):
196
216
  if header:
197
217
  continue
198
218
  header = True
219
+ line = line.strip("│")
220
+ line = line.replace("├─", "└─")
199
221
  if "|" in line:
200
222
  # first column missing '|'
201
223
  rows.append(line.replace("\t", "|"))
202
224
  columns = [row.split("|") for row in rows]
203
225
  df = pd.DataFrame(columns[1:], columns=columns[0])
204
- return df.to_html(index=False, table_id="gpio")
226
+ return df.to_html(index=False, justify="center", col_space=30)
205
227
 
206
228
  def get_prereq_data(self):
207
229
  """Get the prereq data"""
208
230
  prereq = []
209
231
  prereq_debug = []
232
+ tables = [
233
+ "int|active",
234
+ "ACPI name",
235
+ "PCI Slot",
236
+ "DMI|value",
237
+ ]
210
238
  ts = self.db.get_last_prereq_ts()
211
239
  if not ts:
212
240
  return [], "", []
@@ -216,23 +244,24 @@ class SleepReport(AmdTool):
216
244
  if self.debug:
217
245
  for row in self.db.report_debug(t0):
218
246
  content = row[0]
219
- if self.format == "html" and "int|active" in content:
220
- content = self.convert_gpio_dataframe(content)
221
- prereq_debug.append(
222
- {"data": "{message}".format(message=content.strip())}
223
- )
247
+ if self.format == "html" and [
248
+ table for table in tables if table in content
249
+ ]:
250
+ content = self.convert_table_dataframe(content)
251
+ prereq_debug.append({"data": f"{content.strip()}"})
224
252
  return prereq, t0, prereq_debug
225
253
 
226
254
  def get_cycle_data(self):
227
255
  """Get the cycle data"""
228
256
  cycles = []
229
257
  debug = []
258
+ tables = ["Wakeup Source"]
230
259
  num = 0
231
260
  for cycle in self.df["Start Time"]:
232
261
  if self.format == "html":
233
262
  data = ""
234
263
  for line in self.db.report_cycle_data(cycle).split("\n"):
235
- data += "<p>{line}</p>".format(line=line)
264
+ data += f"<p>{line}</p>"
236
265
  cycles.append({"cycle_num": num, "data": data})
237
266
  else:
238
267
  cycles.append([num, self.db.report_cycle_data(cycle)])
@@ -240,7 +269,12 @@ class SleepReport(AmdTool):
240
269
  messages = []
241
270
  priorities = []
242
271
  for row in self.db.report_debug(cycle):
243
- messages.append(row[0])
272
+ content = row[0]
273
+ if self.format == "html" and [
274
+ table for table in tables if table in content
275
+ ]:
276
+ content = self.convert_table_dataframe(content)
277
+ messages.append(content)
244
278
  priorities.append(get_log_priority(row[1]))
245
279
  debug.append(
246
280
  {"cycle_num": num, "messages": messages, "priorities": priorities}
@@ -265,58 +299,69 @@ class SleepReport(AmdTool):
265
299
  prereq, prereq_date, prereq_debug = self.get_prereq_data()
266
300
 
267
301
  # Load the cycle and/or debug data
268
- cycles, debug = self.get_cycle_data()
269
-
270
- self.post_process_dataframe()
271
- failures = None
272
- if self.format == "md":
273
- summary = self.df.to_markdown(floatfmt=".02f")
274
- cycle_data = tabulate(cycles, headers=["Cycle", "data"], tablefmt="pipe")
275
- if self.failures:
276
- failures = tabulate(
277
- self.failures,
278
- headers=["Cycle", "Problem", "Explanation"],
279
- tablefmt="pipe",
302
+ if not self.df.empty:
303
+ cycles, debug = self.get_cycle_data()
304
+
305
+ self.post_process_dataframe()
306
+ failures = None
307
+ if self.format == "md":
308
+ summary = self.df.to_markdown(floatfmt=".02f")
309
+ cycle_data = tabulate(
310
+ cycles, headers=["Cycle", "data"], tablefmt="pipe"
280
311
  )
281
- elif self.format == "txt":
282
- summary = tabulate(self.df, headers=self.df.columns, tablefmt="fancy_grid")
283
- cycle_data = tabulate(
284
- cycles, headers=["Cycle", "data"], tablefmt="fancy_grid"
285
- )
286
- if self.failures:
287
- failures = tabulate(
288
- self.failures,
289
- headers=["Cycle", "Problem", "Explanation"],
290
- tablefmt="fancy_grid",
312
+ if self.failures:
313
+ failures = tabulate(
314
+ self.failures,
315
+ headers=["Cycle", "Problem", "Explanation"],
316
+ tablefmt="pipe",
317
+ )
318
+ elif self.format == "txt":
319
+ summary = tabulate(
320
+ self.df, headers=self.df.columns, tablefmt="fancy_grid"
321
+ )
322
+ cycle_data = tabulate(
323
+ cycles, headers=["Cycle", "data"], tablefmt="fancy_grid"
291
324
  )
292
- elif self.format == "html":
293
- summary = ""
294
- row = 0
295
- # we will use javascript to highlight the high values
296
- for line in self.df.to_html(table_id="summary", render_links=True).split(
297
- "\n"
298
- ):
299
- if "<tr>" in line:
300
- line = line.replace(
301
- "<tr>",
302
- '<tr class="row-low" onclick="pick_summary_cycle(%d)">' % row,
325
+ if self.failures:
326
+ failures = tabulate(
327
+ self.failures,
328
+ headers=["Cycle", "Problem", "Explanation"],
329
+ tablefmt="fancy_grid",
303
330
  )
304
- row = row + 1
305
- summary += line
306
- cycle_data = cycles
307
- failures = self.failures
308
- # only show one cycle in stdout output even if we found more
309
- else:
310
- df = self.df.tail(1)
311
- summary = tabulate(
312
- df, headers=self.df.columns, tablefmt="fancy_grid", showindex=False
313
- )
314
- if cycles[-1][0] == df.index.start:
315
- cycle_data = cycles[-1][-1]
331
+ elif self.format == "html":
332
+ summary = ""
333
+ row = 0
334
+ # we will use javascript to highlight the high values
335
+ for line in self.df.to_html(
336
+ table_id="summary", render_links=True
337
+ ).split("\n"):
338
+ if "<tr>" in line:
339
+ line = line.replace(
340
+ "<tr>",
341
+ f'<tr class="row-low" onclick="pick_summary_cycle({row})">',
342
+ )
343
+ row = row + 1
344
+ summary += line
345
+ cycle_data = cycles
346
+ failures = self.failures
347
+ # only show one cycle in stdout output even if we found more
316
348
  else:
317
- cycle_data = None
318
- if self.failures and self.failures[-1][0] == df.index.start:
319
- failures = self.failures[-1][-1]
349
+ df = self.df.tail(1)
350
+ summary = tabulate(
351
+ df, headers=self.df.columns, tablefmt="fancy_grid", showindex=False
352
+ )
353
+ if cycles[-1][0] == df.index.start:
354
+ cycle_data = cycles[-1][-1]
355
+ else:
356
+ cycle_data = None
357
+ if self.failures and self.failures[-1][0] == df.index.start:
358
+ failures = self.failures[-1][-1]
359
+ else:
360
+ cycles = []
361
+ debug = []
362
+ cycle_data = []
363
+ summary = "No sleep cycles found in the database."
364
+ failures = None
320
365
 
321
366
  # let it burn
322
367
  context = {
@@ -333,7 +378,7 @@ class SleepReport(AmdTool):
333
378
  "failures": failures,
334
379
  }
335
380
  if self.fname:
336
- with open(self.fname, "w") as f:
381
+ with open(self.fname, "w", encoding="utf-8") as f:
337
382
  f.write(template.render(context))
338
383
  if "SUDO_UID" in os.environ:
339
384
  os.chown(
@@ -345,30 +390,30 @@ class SleepReport(AmdTool):
345
390
 
346
391
  def build_battery_chart(self):
347
392
  """Build a battery chart using matplotlib and seaborn"""
348
- import matplotlib.pyplot as plt
349
- import seaborn as sns
350
- import io
393
+ import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel
394
+ import seaborn as sns # pylint: disable=import-outside-toplevel
395
+ import io # pylint: disable=import-outside-toplevel
351
396
 
352
397
  if "Battery Ave Rate" not in self.df.columns:
353
398
  return
354
399
 
355
400
  plt.set_loglevel("warning")
356
- fig, ax1 = plt.subplots()
357
- lns3 = ax1.plot(
401
+ _fig, ax1 = plt.subplots()
402
+ ax1.plot(
358
403
  self.df["Battery Ave Rate"], color="green", label="Charge/Discharge Rate"
359
404
  )
360
405
 
361
406
  ax2 = ax1.twinx()
362
- lns1 = sns.barplot(
407
+ sns.barplot(
363
408
  x=self.df.index,
364
409
  y=self.df["Battery Delta"],
365
410
  color="grey",
366
411
  label="Battery Change",
367
412
  alpha=0.3,
368
413
  )
369
- max = int(len(self.df.index) / 10)
370
- if max:
371
- ax1.set_xticks(range(0, len(self.df.index), max))
414
+ max_range = int(len(self.df.index) / 10)
415
+ if max_range:
416
+ ax1.set_xticks(range(0, len(self.df.index), max_range))
372
417
  ax1.set_xlabel("Cycle")
373
418
  ax1.set_ylabel("Rate (Watts)")
374
419
  ax2.set_ylabel("Battery Change (%)")
@@ -385,20 +430,20 @@ class SleepReport(AmdTool):
385
430
 
386
431
  def build_hw_sleep_chart(self):
387
432
  """Build the hardware sleep chart using matplotlib and seaborn"""
388
- import matplotlib.pyplot as plt
389
- import seaborn as sns
390
- import io
433
+ import matplotlib.pyplot as plt # pylint: disable=import-outside-toplevel
434
+ import seaborn as sns # pylint: disable=import-outside-toplevel
435
+ import io # pylint: disable=import-outside-toplevel
391
436
 
392
437
  plt.set_loglevel("warning")
393
- fig, ax1 = plt.subplots()
394
- lns3 = ax1.plot(
438
+ _fig, ax1 = plt.subplots()
439
+ ax1.plot(
395
440
  self.df["Hardware Sleep"],
396
441
  color="red",
397
442
  label="Hardware Sleep",
398
443
  )
399
444
 
400
445
  ax2 = ax1.twinx()
401
- lns1 = sns.barplot(
446
+ sns.barplot(
402
447
  x=self.df.index,
403
448
  y=self.df["Duration"] / 60,
404
449
  color="grey",
@@ -406,9 +451,9 @@ class SleepReport(AmdTool):
406
451
  alpha=0.3,
407
452
  )
408
453
 
409
- max = int(len(self.df.index) / 10)
410
- if max:
411
- ax1.set_xticks(range(0, len(self.df.index), max))
454
+ max_range = int(len(self.df.index) / 10)
455
+ if max_range:
456
+ ax1.set_xticks(range(0, len(self.df.index), max_range))
412
457
  ax1.set_xlabel("Cycle")
413
458
  ax1.set_ylabel("Percent")
414
459
  ax2.set_yscale("log")
@@ -427,15 +472,13 @@ class SleepReport(AmdTool):
427
472
  def run(self, inc_prereq=True):
428
473
  """Run the report"""
429
474
 
430
- if self.df.empty:
431
- raise ValueError(f"No data found between {self.since} and {self.until}")
432
-
433
475
  characters = print_temporary_message("Building report, please wait...")
434
476
 
435
- # Build charts in the page for html format
436
- if len(self.df.index) > 1 and self.format == "html":
437
- self.build_battery_chart()
438
- self.build_hw_sleep_chart()
477
+ if not self.df.empty:
478
+ # Build charts in the page for html format
479
+ if len(self.df.index) > 1 and self.format == "html":
480
+ self.build_battery_chart()
481
+ self.build_hw_sleep_chart()
439
482
 
440
483
  # Render the template using jinja
441
484
  msg = self.build_template(inc_prereq)
amd_debug/templates/html CHANGED
@@ -42,12 +42,14 @@
42
42
  table,
43
43
  th,
44
44
  td {
45
- border-width: 0;
45
+ border-width: 1;
46
+ border-collapse: collapse;
46
47
  table-layout: fixed;
47
48
  font-family: sans-serif;
48
49
  letter-spacing: 0.02em;
49
50
  color: #000000;
50
- margin-bottom: 10px;
51
+ text-align: left;
52
+ padding: 3px;
51
53
  }
52
54
 
53
55
  .○ {
amd_debug/validator.py CHANGED
@@ -290,13 +290,12 @@ class SleepValidator(AmdTool):
290
290
  sys_name = pnp.sys_name
291
291
 
292
292
  name = name.replace('"', "")
293
- devices.append(f"{name} [{sys_name}]: {wake_en}")
293
+ devices.append(f"{name}|{sys_name}|{wake_en}")
294
294
  devices.sort()
295
- self.db.record_debug("Possible wakeup sources:")
295
+ debug_str = "Wakeup Source|Linux Device|Status\n"
296
296
  for dev in devices:
297
- # set prefix if last device
298
- prefix = "│ " if dev != devices[-1] else "└─"
299
- self.db.record_debug(f"{prefix}{dev}")
297
+ debug_str += f"{dev}\n"
298
+ self.db.record_debug(debug_str)
300
299
 
301
300
  def capture_lid(self) -> None:
302
301
  """Capture lid state"""
@@ -747,16 +746,18 @@ class SleepValidator(AmdTool):
747
746
  return False
748
747
  else:
749
748
  old = get_wakeup_count()
749
+ p = os.path.join("/", "sys", "power", "state")
750
+ fd = os.open(p, os.O_WRONLY | os.O_SYNC)
750
751
  try:
751
- p = os.path.join("/", "sys", "power", "state")
752
- with open(p, "w", encoding="utf-8") as w:
753
- w.write("mem")
752
+ os.write(fd, b"mem")
754
753
  except OSError as e:
755
754
  new = get_wakeup_count()
756
755
  self.db.record_cycle_data(
757
756
  f"Failed to set suspend state ({old} -> {new}): {e}", "❌"
758
757
  )
759
758
  return False
759
+ finally:
760
+ os.close(fd)
760
761
  return True
761
762
 
762
763
  def unlock_session(self):
@@ -780,6 +781,7 @@ class SleepValidator(AmdTool):
780
781
 
781
782
  def run(self, duration, count, wait, rand, logind):
782
783
  """Run the suspend test"""
784
+ min_duration = 4
783
785
  if not count:
784
786
  return True
785
787
 
@@ -787,8 +789,13 @@ class SleepValidator(AmdTool):
787
789
  self.logind = True
788
790
 
789
791
  if rand:
792
+ if duration <= min_duration:
793
+ print_color(f"Invalid max duration {duration}", "❌")
794
+ self.db.sync()
795
+ self.report_cycle()
796
+ return False
790
797
  print_color(
791
- f"Running {count} cycle random test with max duration of {duration}s and a max wait of {wait}s",
798
+ f"Running {count} cycle random test with min duration of {min_duration}s, max duration of {duration}s and a max wait of {wait}s",
792
799
  "🗣️",
793
800
  )
794
801
  elif count > 1:
@@ -799,7 +806,7 @@ class SleepValidator(AmdTool):
799
806
  )
800
807
  for i in range(1, count + 1):
801
808
  if rand:
802
- self.requested_duration = random.randint(1, duration)
809
+ self.requested_duration = random.randint(min_duration, duration)
803
810
  requested_wait = random.randint(1, wait)
804
811
  else:
805
812
  self.requested_duration = duration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amd-debug-tools
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: debug tools for AMD systems
5
5
  Author-email: Mario Limonciello <superm1@kernel.org>
6
6
  License-Expression: MIT
@@ -9,11 +9,11 @@ test_failures.py,sha256=H1UxXeVjhJs9-j9yas4vwAha676GX1Es7Kz8RN2B590,6845
9
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=4Ltxx7vGU91b9MX1AtdPLS8iyWJBCNuEKCuO0nnEb_g,83381
12
+ test_prerequisites.py,sha256=VXN822W-7ZZHXZkJYH5MeKsDVBC-ttUZggYlcoPjVyM,83335
13
13
  test_pstate.py,sha256=a9oAJ9-LANX32XNQhplz6Y75VNYc__QqoSBKIrwvANg,6058
14
- test_s2idle.py,sha256=-S3yymjprf3_LIgaPGWgzN9FmnyQG55dNc4xqWAF8Z8,32344
15
- test_sleep_report.py,sha256=R3cUPPT9r9q4q93xk6NFvi4ySgT5laqidk2SASuTWIo,5878
16
- test_validator.py,sha256=NeiX8kT5DiHiiB5lRSJkjIpV32UzaIrW5ljKLmFtBMk,30490
14
+ test_s2idle.py,sha256=6NaqGp9VOLr_Tr3KczSvfSo3M882aYEbSvRV9xvUMcA,33534
15
+ test_sleep_report.py,sha256=ANuxYi_C1oSKAi4xUU2wBu4SwJtcZA7VPpazBe3_WUQ,6922
16
+ test_validator.py,sha256=-MfrWfhwef_aRqOSD_dJGhH0shsghhtOBgzeijzyLW4,33975
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
@@ -22,24 +22,24 @@ amd_debug/bios.py,sha256=wmPKDsTZeQqsHjWpv-YHdgRNlCtFdzHQ6jJf0H3hjN8,3971
22
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=Otv3YDu7Je4ljSifVmvjObGoOY4OvLIY20pw-v4Dqkw,22911
25
+ amd_debug/failures.py,sha256=z4O4Q-akv3xYGssSZFCqE0cDE4P9F_aw1hxil3McoD4,22910
26
26
  amd_debug/installer.py,sha256=r6r_nVWv8qYdrqAvnAzQhRiS5unBDOkXsqUfHvFK8uM,14249
27
- amd_debug/kernel.py,sha256=xzAy-sDY5-sd4jxyU7EaBokS7YsvEjoWRuexaTJNRBc,11851
28
- amd_debug/prerequisites.py,sha256=bKJA9ztyapB8rxrNEgc-hxazw5Uh-sP5X0S7zplGA0c,50413
27
+ amd_debug/kernel.py,sha256=UAlxlXNuZxtHVtrfCmTp12YombVaUs4mizOxwuXTX2M,12038
28
+ amd_debug/prerequisites.py,sha256=r4_IFTL-1YcPptlt6Nump7iscRx1bBCGF33hqNQB0X0,49867
29
29
  amd_debug/pstate.py,sha256=akGdJkIxBp0bx3AeGv6ictNxwv8m0j9vQ2IZB0Jx3dM,9518
30
30
  amd_debug/s2idle-hook,sha256=LLiaqPtGd0qetu9n6EYxKHZaIdHpVQDONdOuSc0pfFg,1695
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
31
+ amd_debug/s2idle.py,sha256=Ei5ONnJyHz9aQbstRZYnofhJ_sJOAOZQxLgIuWfvcng,13218
32
+ amd_debug/sleep_report.py,sha256=hhqu711AKtjeYF2xmGcejyCyyPtmq4-gC_hROUCrC0g,17317
33
+ amd_debug/validator.py,sha256=nZ5UpvvsABw4yTea_pZ8DHaLbSoM5VHz3uvlrdWAb34,33444
34
34
  amd_debug/wake.py,sha256=xT8WrFrN6voCmXWo5dsn4mQ7iR2QJxHrrYBd3EREG-Q,3936
35
35
  amd_debug/bash/amd-s2idle,sha256=g_cle1ElCJpwE4wcLezL6y-BdasDKTnNMhrtzKLE9ks,1142
36
- amd_debug/templates/html,sha256=tnpqHDZF5FfhC6YNRUfOG6Vn9ZtISFr10kEXSB476Mw,14518
36
+ amd_debug/templates/html,sha256=JfGhpmHIB2C2GItdGI1kuC8uayqEVgrpQvAWAj35eZ4,14580
37
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.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,,
40
+ amd_debug_tools-0.2.3.dist-info/licenses/LICENSE,sha256=RBlZI6r3MRGzymI2VDX2iW__D2APDbMhu_Xg5t6BWeo,1066
41
+ amd_debug_tools-0.2.3.dist-info/METADATA,sha256=-DdxkPvWEXMifkgAjfyNIpleo_rHFPIH8nS6v3AWJTk,6877
42
+ amd_debug_tools-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
+ amd_debug_tools-0.2.3.dist-info/entry_points.txt,sha256=HC11T2up0pPfroAn6Pg5M2jOZXhkWIipToJ1YPTKqu8,116
44
+ amd_debug_tools-0.2.3.dist-info/top_level.txt,sha256=XYjxExbUTEtiIlag_5iQvZSVOC1EIxhKM4NLklReQ0k,234
45
+ amd_debug_tools-0.2.3.dist-info/RECORD,,
test_prerequisites.py CHANGED
@@ -500,9 +500,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
500
500
  result = self.validator.map_acpi_path()
501
501
  self.assertTrue(result)
502
502
  self.mock_db.record_debug.assert_called_with(
503
- "ACPI name: ACPI path [driver]\n"
504
- "│ device1: mocked_path [driver]\n"
505
- "└─device2: mocked_path [driver]\n"
503
+ "ACPI name | ACPI path | Kernel driver\ndevice1 | mocked_path | driver\ndevice2 | mocked_path | driver\n"
506
504
  )
507
505
 
508
506
  @patch("amd_debug.prerequisites.os.path.exists")
@@ -545,7 +543,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
545
543
  result = self.validator.map_acpi_path()
546
544
  self.assertTrue(result)
547
545
  self.mock_db.record_debug.assert_called_with(
548
- "ACPI name: ACPI path [driver]\n└─device1: mocked_path [None]\n"
546
+ "ACPI name | ACPI path | Kernel driver\ndevice1 | mocked_path | None\n"
549
547
  )
550
548
 
551
549
  @patch("amd_debug.prerequisites.read_file")
@@ -569,8 +567,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
569
567
 
570
568
  self.validator.capture_pci_acpi()
571
569
  self.mock_db.record_debug.assert_called_with(
572
- "PCI devices\n"
573
- "└─0000:00:1f.0 : Intel Corporation ISA bridge [1234abcd] : mocked_acpi_path\n"
570
+ "PCI Slot | Vendor | Class | ID | ACPI path\n└─0000:00:1f.0 | Intel Corporation | ISA bridge | 1234abcd | mocked_acpi_path\n"
574
571
  )
575
572
 
576
573
  @patch("amd_debug.prerequisites.read_file")
@@ -593,8 +590,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
593
590
 
594
591
  self.validator.capture_pci_acpi()
595
592
  self.mock_db.record_debug.assert_called_with(
596
- "PCI devices\n"
597
- "└─0000:01:00.0 : NVIDIA Corporation VGA compatible controller [5678efgh]\n"
593
+ "PCI Slot | Vendor | Class | ID | ACPI path\n└─0000:01:00.0 | NVIDIA Corporation | VGA compatible controller | 5678efgh | \n"
598
594
  )
599
595
 
600
596
  @patch("amd_debug.prerequisites.read_file")
@@ -628,9 +624,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
628
624
 
629
625
  self.validator.capture_pci_acpi()
630
626
  self.mock_db.record_debug.assert_called_with(
631
- "PCI devices\n"
632
- "│ 0000:00:1f.0 : Intel Corporation ISA bridge [1234abcd] : mocked_acpi_path\n"
633
- "└─0000:01:00.0 : NVIDIA Corporation VGA compatible controller [5678efgh] : mocked_acpi_path\n"
627
+ "PCI Slot | Vendor | Class | ID | ACPI path\n│ 0000:00:1f.0 | Intel Corporation | ISA bridge | 1234abcd | mocked_acpi_path\n└─0000:01:00.0 | NVIDIA Corporation | VGA compatible controller | 5678efgh | mocked_acpi_path\n"
634
628
  )
635
629
 
636
630
  def test_capture_pci_acpi_no_devices(self):
@@ -638,7 +632,9 @@ class TestPrerequisiteValidator(unittest.TestCase):
638
632
  self.mock_pyudev.list_devices.return_value = []
639
633
 
640
634
  self.validator.capture_pci_acpi()
641
- self.mock_db.record_debug.assert_called_with("PCI devices\n")
635
+ self.mock_db.record_debug.assert_called_with(
636
+ "PCI Slot | Vendor | Class | ID | ACPI path\n"
637
+ )
642
638
 
643
639
  @patch("amd_debug.prerequisites.read_file")
644
640
  def test_check_aspm_default_policy(self, mock_read_file):
@@ -1006,7 +1002,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
1006
1002
  "MockVendor MockProduct (MockFamily)", "💻"
1007
1003
  )
1008
1004
  self.mock_db.record_debug.assert_called_with(
1009
- "DMI data:\nchassis_type: Desktop\n"
1005
+ "DMI|value\nchassis_type| Desktop\n"
1010
1006
  )
1011
1007
 
1012
1008
  @patch("amd_debug.prerequisites.os.walk")
@@ -1034,7 +1030,7 @@ class TestPrerequisiteValidator(unittest.TestCase):
1034
1030
  self.mock_db.record_prereq.assert_called_with(
1035
1031
  "MockVendor MockProduct (MockFamily)", "💻"
1036
1032
  )
1037
- self.mock_db.record_debug.assert_called_with("DMI data:\n")
1033
+ self.mock_db.record_debug.assert_called_with("DMI|value\n")
1038
1034
 
1039
1035
  @patch("amd_debug.prerequisites.os.walk")
1040
1036
  @patch("amd_debug.prerequisites.read_file")
@@ -1710,9 +1706,6 @@ class TestPrerequisiteValidator(unittest.TestCase):
1710
1706
  ]
1711
1707
  result = self.validator.check_storage()
1712
1708
  self.assertTrue(result)
1713
- self.mock_db.record_debug.assert_called_with(
1714
- "New enough kernel to avoid NVME check"
1715
- )
1716
1709
 
1717
1710
  def test_check_storage_no_kernel_log(self):
1718
1711
  """Test check_storage when kernel log is unavailable"""
test_s2idle.py CHANGED
@@ -455,12 +455,18 @@ class TestTestFunction(unittest.TestCase):
455
455
 
456
456
  @patch("amd_debug.s2idle.Installer")
457
457
  @patch("amd_debug.s2idle.PrerequisiteValidator")
458
- @patch("amd_debug.prerequisites.SleepDatabase")
459
- @patch("amd_debug.validator.SleepDatabase")
458
+ @patch("amd_debug.s2idle.SleepValidator")
459
+ @patch("amd_debug.s2idle.SleepReport")
460
+ @patch("amd_debug.s2idle.prompt_test_arguments")
461
+ @patch("amd_debug.s2idle.prompt_report_arguments")
462
+ @patch("amd_debug.s2idle.display_report_file")
460
463
  def test_test_prerequisite_failure(
461
464
  self,
462
- _mock_sleep_db,
463
- _mock_sleep_db_prerequisite,
465
+ mock_display_report_file,
466
+ mock_prompt_report_arguments,
467
+ mock_prompt_test_arguments,
468
+ mock_sleep_report,
469
+ mock_sleep_validator,
464
470
  mock_prerequisite_validator,
465
471
  mock_installer,
466
472
  ):
@@ -471,6 +477,17 @@ class TestTestFunction(unittest.TestCase):
471
477
  mock_prerequisite_instance = mock_prerequisite_validator.return_value
472
478
  mock_prerequisite_instance.run.return_value = False
473
479
 
480
+ mock_prompt_test_arguments.return_value = (10, 5, 3)
481
+ mock_prompt_report_arguments.return_value = (
482
+ "2023-01-01",
483
+ "2023-02-01",
484
+ "report.html",
485
+ "html",
486
+ True,
487
+ )
488
+ mock_sleep_validator_instance = mock_sleep_validator.return_value
489
+ mock_sleep_report_instance = mock_sleep_report.return_value
490
+
474
491
  result = run_test_cycle(
475
492
  duration=None,
476
493
  wait=None,
@@ -492,7 +509,20 @@ class TestTestFunction(unittest.TestCase):
492
509
  mock_prerequisite_validator.assert_called_once_with(True)
493
510
  mock_prerequisite_instance.run.assert_called_once()
494
511
  mock_prerequisite_instance.report.assert_called_once()
495
- self.assertFalse(result)
512
+ mock_prompt_test_arguments.assert_called_once_with(None, None, None, False)
513
+ mock_prompt_report_arguments.assert_called_once()
514
+ mock_sleep_validator_instance.assert_not_called()
515
+ mock_sleep_report.assert_called_once_with(
516
+ since=None,
517
+ until=None,
518
+ fname="report.html",
519
+ fmt="html",
520
+ tool_debug=True,
521
+ report_debug=True,
522
+ )
523
+ mock_sleep_report_instance.run.assert_called_once()
524
+ mock_display_report_file.assert_called_once_with("report.html", "html")
525
+ self.assertTrue(result)
496
526
 
497
527
  @patch("amd_debug.s2idle.Installer")
498
528
  @patch("amd_debug.s2idle.PrerequisiteValidator")
@@ -533,9 +563,9 @@ class TestTestFunction(unittest.TestCase):
533
563
  "iasl", "ethtool", "edid-decode"
534
564
  )
535
565
  mock_installer_instance.install_dependencies.assert_called_once()
536
- mock_prerequisite_validator.assert_called_once_with(True)
537
- mock_prerequisite_instance.run.assert_called_once()
538
- mock_prerequisite_instance.report.assert_called_once()
566
+ mock_prerequisite_validator.assert_not_called()
567
+ mock_prerequisite_instance.run.assert_not_called()
568
+ mock_prerequisite_instance.report.assert_not_called()
539
569
  mock_prompt_test_arguments.assert_called_once_with(None, None, None, False)
540
570
 
541
571
 
test_sleep_report.py CHANGED
@@ -5,6 +5,7 @@
5
5
  This module contains unit tests for the s2idle tool in the amd-debug-tools package.
6
6
  """
7
7
 
8
+ import math
8
9
  import unittest
9
10
  from datetime import datetime
10
11
  from unittest.mock import patch
@@ -165,3 +166,31 @@ class TestSleepReport(unittest.TestCase):
165
166
  self.report.build_hw_sleep_chart()
166
167
  self.assertIsNotNone(self.report.hwsleep_svg)
167
168
  mock_savefig.assert_called_once()
169
+
170
+ def test_pre_process_dataframe_zero_duration(self):
171
+ """Test the pre_process_dataframe method when t0 and t1 are the same."""
172
+ # Mock the dataframe with t0 and t1 being the same
173
+ self.report.df = pd.DataFrame(
174
+ {
175
+ "t0": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")],
176
+ "t1": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")],
177
+ "hw": [50],
178
+ "requested": [1],
179
+ "gpio": ["1, 2"],
180
+ "wake_irq": ["1"],
181
+ "b0": [90],
182
+ "b1": [85],
183
+ "full": [100],
184
+ }
185
+ )
186
+
187
+ # Call the method
188
+ self.report.pre_process_dataframe()
189
+
190
+ # Verify the dataframe was processed correctly
191
+ self.assertTrue(
192
+ self.report.df["Duration"].isna().iloc[0]
193
+ ) # Duration should be NaN
194
+ self.assertTrue(
195
+ math.isnan(self.report.df["Hardware Sleep"].iloc[0])
196
+ ) # Hardware Sleep should be NaN
test_validator.py CHANGED
@@ -5,8 +5,9 @@
5
5
  This module contains unit tests for the validator functions in the amd-debug-tools package.
6
6
  """
7
7
 
8
- from unittest.mock import patch, mock_open
8
+ from unittest.mock import patch, mock_open, Mock
9
9
 
10
+ import os
10
11
  import logging
11
12
  import unittest
12
13
  import math
@@ -157,9 +158,8 @@ class TestValidator(unittest.TestCase):
157
158
  self.validator.capture_wake_sources()
158
159
 
159
160
  # Validate debug messages
160
- mock_record_debug.assert_any_call("Possible wakeup sources:")
161
161
  mock_record_debug.assert_any_call(
162
- "└─ [/sys/devices/pci0000:00/0000:00:14.0]: enabled"
162
+ "Wakeup Source|Linux Device|Status\n|/sys/devices/pci0000:00/0000:00:14.0|enabled\n"
163
163
  )
164
164
 
165
165
  # Stop patches
@@ -694,7 +694,7 @@ class TestValidator(unittest.TestCase):
694
694
  # Test case 3: Randomized test
695
695
  mock_randint.side_effect = [7, 3] # Random duration and wait
696
696
  self.validator.run(duration=10, count=1, wait=5, rand=True, logind=False)
697
- mock_randint.assert_any_call(1, 10)
697
+ mock_randint.assert_any_call(4, 10)
698
698
  mock_randint.assert_any_call(1, 5)
699
699
  mock_run_countdown.assert_any_call("Suspending system", math.ceil(3 / 2))
700
700
  mock_run_countdown.assert_any_call("Collecting data", math.ceil(3 / 2))
@@ -705,19 +705,128 @@ class TestValidator(unittest.TestCase):
705
705
  mock_report_cycle.assert_called()
706
706
  mock_unlock_session.assert_called()
707
707
 
708
- # Test case 4: Multiple cycles
708
+ # Test case 4: Randomized test, but too short of a duration
709
+ result = self.validator.run(
710
+ duration=4, count=1, wait=5, rand=True, logind=False
711
+ )
712
+ self.assertFalse(result)
713
+ mock_report_cycle.assert_called()
714
+
715
+ # Test case 5: Multiple cycles
709
716
  self.validator.run(duration=10, count=2, wait=5, rand=False, logind=False)
710
717
  self.assertEqual(mock_prep.call_count, 4) # Includes previous calls
711
718
  self.assertEqual(mock_program_wakealarm.call_count, 4)
712
719
  self.assertEqual(mock_suspend_system.call_count, 4)
713
720
  self.assertEqual(mock_post.call_count, 4)
714
- self.assertEqual(mock_report_cycle.call_count, 4)
721
+ self.assertEqual(mock_report_cycle.call_count, 5)
715
722
  self.assertEqual(mock_unlock_session.call_count, 3)
716
723
 
717
- # Test case 5: suspend_system fails
724
+ # Test case 6: suspend_system fails
718
725
  mock_suspend_system.return_value = False
719
726
  result = self.validator.run(
720
727
  duration=10, count=1, wait=5, rand=False, logind=False
721
728
  )
722
729
  self.assertFalse(result)
723
730
  mock_report_cycle.assert_called()
731
+
732
+ @patch("os.path.exists")
733
+ @patch("builtins.open", new_callable=mock_open, read_data="3")
734
+ @patch("os.write")
735
+ @patch("os.open")
736
+ @patch("os.close")
737
+ def test_suspend_system_sysfs_success(
738
+ self,
739
+ mock_os_close,
740
+ mock_os_open,
741
+ mock_os_write,
742
+ _mock_open_file,
743
+ mock_path_exists,
744
+ ):
745
+ """Test suspend_system method using sysfs interface with success"""
746
+ # Mock wakeup_count file existence
747
+ mock_path_exists.side_effect = lambda path: "wakeup_count" in path
748
+
749
+ # Mock os.open and os.write
750
+ mock_os_open.return_value = 3
751
+ mock_os_write.return_value = None
752
+
753
+ # Call the method
754
+ result = self.validator.suspend_system()
755
+
756
+ # Assert the method returned True
757
+ self.assertTrue(result)
758
+
759
+ # Assert os.open and os.write were called
760
+ mock_os_open.assert_called_once_with(
761
+ "/sys/power/state", os.O_WRONLY | os.O_SYNC
762
+ )
763
+ mock_os_write.assert_called_once_with(3, b"mem")
764
+ mock_os_close.assert_called_once_with(3)
765
+
766
+ @patch("os.path.exists")
767
+ @patch("builtins.open", new_callable=mock_open, read_data="3")
768
+ @patch("os.write")
769
+ @patch("os.open")
770
+ @patch("os.close")
771
+ def test_suspend_system_sysfs_failure(
772
+ self,
773
+ mock_os_close,
774
+ mock_os_open,
775
+ mock_os_write,
776
+ _mock_open_file,
777
+ mock_path_exists,
778
+ ):
779
+ """Test suspend_system method using sysfs interface with failure"""
780
+ # Mock wakeup_count file existence
781
+ mock_path_exists.side_effect = lambda path: "wakeup_count" in path
782
+
783
+ # Mock os.open to raise OSError
784
+ mock_os_open.return_value = 3
785
+ mock_os_write.side_effect = OSError("Failed to write to state")
786
+
787
+ # Call the method
788
+ result = self.validator.suspend_system()
789
+
790
+ # Assert the method returned False
791
+ self.assertFalse(result)
792
+
793
+ # Assert os.open and os.write were called
794
+ mock_os_open.assert_called_once_with(
795
+ "/sys/power/state", os.O_WRONLY | os.O_SYNC
796
+ )
797
+ mock_os_write.assert_called_once_with(3, b"mem")
798
+ mock_os_close.assert_called_once_with(3)
799
+
800
+ @patch("os.path.exists")
801
+ @patch("builtins.open", new_callable=mock_open)
802
+ @patch("os.write")
803
+ @patch("os.open")
804
+ @patch("os.close")
805
+ def test_suspend_system_sysfs_no_wakeup_count(
806
+ self,
807
+ mock_os_close,
808
+ mock_os_open,
809
+ mock_os_write,
810
+ _mock_open_file,
811
+ mock_path_exists,
812
+ ):
813
+ """Test suspend_system method using sysfs interface with no wakeup_count file"""
814
+ # Mock wakeup_count file does not exist
815
+ mock_path_exists.return_value = False
816
+
817
+ # Mock os.open and os.write
818
+ mock_os_open.return_value = 3
819
+ mock_os_write.return_value = None
820
+
821
+ # Call the method
822
+ result = self.validator.suspend_system()
823
+
824
+ # Assert the method returned True
825
+ self.assertTrue(result)
826
+
827
+ # Assert os.open and os.write were called
828
+ mock_os_open.assert_called_once_with(
829
+ "/sys/power/state", os.O_WRONLY | os.O_SYNC
830
+ )
831
+ mock_os_write.assert_called_once_with(3, b"mem")
832
+ mock_os_close.assert_called_once_with(3)