amd-debug-tools 0.2.0__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.
test_validator.py ADDED
@@ -0,0 +1,723 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ This module contains unit tests for the validator functions in the amd-debug-tools package.
6
+ """
7
+
8
+ from unittest.mock import patch, mock_open
9
+
10
+ import logging
11
+ import unittest
12
+ import math
13
+ from datetime import datetime
14
+
15
+ from amd_debug.validator import pm_debugging, soc_needs_irq1_wa, SleepValidator
16
+
17
+
18
+ class TestValidatorHelpers(unittest.TestCase):
19
+ """Test validator Helper functions"""
20
+
21
+ @classmethod
22
+ def setUpClass(cls):
23
+ logging.basicConfig(filename="/dev/null", level=logging.DEBUG)
24
+
25
+ def test_soc_needs_irq1_wa(self):
26
+ """Test if the SOC should apply an IRQ1 workaround"""
27
+ ret = soc_needs_irq1_wa(0x17, 0x68, "1.2.3")
28
+ self.assertTrue(ret)
29
+ ret = soc_needs_irq1_wa(0x17, 0x69, "1.2.3")
30
+ self.assertFalse(ret)
31
+ ret = soc_needs_irq1_wa(0x19, 0x51, "64.65.0")
32
+ self.assertFalse(ret)
33
+ ret = soc_needs_irq1_wa(0x19, 0x50, "64.65.0")
34
+ self.assertTrue(ret)
35
+ ret = soc_needs_irq1_wa(0x19, 0x50, "64.66.0")
36
+ self.assertFalse(ret)
37
+
38
+ def test_pm_debugging(self):
39
+ """Test pm_debugging decorator"""
40
+
41
+ @pm_debugging
42
+ def test_function():
43
+ return "Test function executed"
44
+
45
+ # Mock /sys/power/pm_debug_messages existing and all ACPI existing
46
+ with patch("builtins.open", new_callable=mock_open, read_data="0") as mock_file:
47
+ handlers = (
48
+ mock_file.return_value,
49
+ mock_open(read_data="0").return_value,
50
+ mock_open(read_data="0").return_value,
51
+ mock_open(read_data="0").return_value,
52
+ )
53
+ mock_open.side_effect = handlers
54
+ result = test_function()
55
+ self.assertEqual(result, "Test function executed")
56
+
57
+ # Mock /sys/power/pm_debug_messages missing
58
+ with patch(
59
+ "builtins.open", side_effect=FileNotFoundError("not found")
60
+ ) as mock_file:
61
+ with self.assertRaises(FileNotFoundError):
62
+ result = test_function()
63
+
64
+
65
+ class TestValidator(unittest.TestCase):
66
+ """Test validator functions"""
67
+
68
+ @classmethod
69
+ def setUpClass(cls):
70
+ logging.basicConfig(filename="/dev/null", level=logging.DEBUG)
71
+
72
+ @patch("amd_debug.validator.SleepDatabase")
73
+ def setUp(self, _db_mock):
74
+ """Set up a mock context for testing"""
75
+ self.validator = SleepValidator(tool_debug=True, bios_debug=False)
76
+
77
+ def test_capture_running_compositors(self):
78
+ """Test capture_running_compositors method"""
79
+ with patch("glob.glob", return_value=["/proc/1234", "/proc/5678"]), patch(
80
+ "os.path.exists", return_value=True
81
+ ), patch(
82
+ "os.readlink", side_effect=["/usr/bin/kwin_wayland", "/usr/bin/gnome-shell"]
83
+ ), patch.object(
84
+ self.validator.db, "record_debug"
85
+ ) as mock_record_debug:
86
+ self.validator.capture_running_compositors()
87
+ mock_record_debug.assert_any_call("kwin_wayland compositor is running")
88
+ mock_record_debug.assert_any_call("gnome-shell compositor is running")
89
+
90
+ def test_capture_power_profile(self):
91
+ """Test capture_power_profile method"""
92
+ with patch("os.path.exists", return_value=True), patch(
93
+ "subprocess.check_output",
94
+ return_value=b"Performance\nBalanced\nPower Saver",
95
+ ), patch.object(self.validator.db, "record_debug") as mock_record_debug:
96
+ self.validator.capture_power_profile()
97
+ mock_record_debug.assert_any_call("Power Profiles:")
98
+ mock_record_debug.assert_any_call("│ Performance")
99
+ mock_record_debug.assert_any_call("│ Balanced")
100
+ mock_record_debug.assert_any_call("└─Power Saver")
101
+
102
+ def test_capture_battery(self):
103
+ """Test capture_battery method"""
104
+ with patch.object(
105
+ self.validator.batteries, "get_batteries", return_value=["BAT0"]
106
+ ), patch.object(
107
+ self.validator.batteries, "get_energy_unit", return_value="µWh"
108
+ ), patch.object(
109
+ self.validator.batteries, "get_energy", return_value=50000
110
+ ), patch.object(
111
+ self.validator.batteries, "get_energy_full", return_value=60000
112
+ ), patch.object(
113
+ self.validator.db, "record_debug"
114
+ ) as mock_record_debug, patch.object(
115
+ self.validator.db, "record_battery_energy"
116
+ ) as mock_record_battery_energy:
117
+ self.validator.capture_battery()
118
+ mock_record_debug.assert_called_with("BAT0 energy level is 50000 µWh")
119
+ mock_record_battery_energy.assert_called_with("BAT0", 50000, 60000, "W")
120
+
121
+ def test_check_rtc_cmos(self):
122
+ """Test check_rtc_cmos method"""
123
+ with patch(
124
+ "os.path.join",
125
+ return_value="/sys/module/rtc_cmos/parameters/use_acpi_alarm",
126
+ ), patch("builtins.open", mock_open(read_data="N")), patch.object(
127
+ self.validator.db, "record_cycle_data"
128
+ ) as mock_record_cycle_data:
129
+ self.validator.check_rtc_cmos()
130
+ mock_record_cycle_data.assert_called_with(
131
+ "`rtc_cmos` not configured to use ACPI alarm", "🚦"
132
+ )
133
+
134
+ def test_capture_wake_sources(self):
135
+ """Test capture_wake_sources method"""
136
+ mock_pyudev = patch.object(self.validator, "pyudev").start()
137
+ mock_record_debug = patch.object(self.validator.db, "record_debug").start()
138
+ mock_read_file = patch("amd_debug.validator.read_file").start()
139
+ mock_os_path_exists = patch("os.path.exists").start()
140
+
141
+ # Mock wakeup devices
142
+ mock_wakeup_device = mock_pyudev.list_devices.return_value = [
143
+ unittest.mock.Mock(
144
+ sys_path="/sys/devices/pci0000:00/0000:00:14.0",
145
+ find_parent=lambda subsystem, **kwargs: None,
146
+ )
147
+ ]
148
+
149
+ # Mock wakeup file existence and content
150
+ mock_os_path_exists.return_value = True
151
+ mock_read_file.return_value = "enabled"
152
+
153
+ # Mock device properties
154
+ mock_wakeup_device[0].properties = {"PCI_CLASS": "0x0c0330"}
155
+ mock_wakeup_device[0].sys_path = "/sys/devices/pci0000:00/0000:00:14.0"
156
+
157
+ self.validator.capture_wake_sources()
158
+
159
+ # Validate debug messages
160
+ mock_record_debug.assert_any_call("Possible wakeup sources:")
161
+ mock_record_debug.assert_any_call(
162
+ "└─ [/sys/devices/pci0000:00/0000:00:14.0]: enabled"
163
+ )
164
+
165
+ # Stop patches
166
+ patch.stopall()
167
+
168
+ def test_capture_lid(self):
169
+ """Test capture_lid method"""
170
+ with patch("os.walk", return_value=[("/", [], ["lid0", "lid1"])]), patch(
171
+ "os.path.join", side_effect=lambda *args: "/".join(args)
172
+ ), patch(
173
+ "amd_debug.validator.read_file",
174
+ side_effect=["state: open", "state: closed"],
175
+ ), patch.object(
176
+ self.validator.db, "record_debug"
177
+ ) as mock_record_debug:
178
+ self.validator.capture_lid()
179
+ mock_record_debug.assert_any_call("ACPI Lid (//lid0): open")
180
+ mock_record_debug.assert_any_call("ACPI Lid (//lid1): closed")
181
+
182
+ def test_capture_wakeup_irq_data(self):
183
+ """Test capture_wakeup_irq_data method"""
184
+ with patch("os.path.join", side_effect=lambda *args: "/".join(args)), patch(
185
+ "amd_debug.validator.read_file",
186
+ side_effect=[
187
+ "123", # IRQ number
188
+ "chip_name_mock", # Chip name
189
+ "irq_name_mock", # IRQ name
190
+ "hw_mock", # Hardware IRQ
191
+ "actions_mock", # Actions
192
+ ],
193
+ ), patch.object(self.validator.db, "record_debug") as mock_record_debug:
194
+ result = self.validator.capture_wakeup_irq_data()
195
+ self.assertTrue(result)
196
+ mock_record_debug.assert_called_once_with(
197
+ "Woke up from IRQ 123 (chip_name_mock hw_mock-irq_name_mock actions_mock)"
198
+ )
199
+
200
+ def test_capture_thermal(self):
201
+ """Test capture_thermal method"""
202
+ # Mock pyudev devices
203
+ mock_pyudev = patch.object(self.validator, "pyudev").start()
204
+ mock_record_debug = patch.object(self.validator.db, "record_debug").start()
205
+ mock_record_prereq = patch.object(self.validator.db, "record_prereq").start()
206
+ mock_read_file = patch("amd_debug.validator.read_file").start()
207
+ mock_os_listdir = patch("os.listdir").start()
208
+
209
+ # Mock thermal devices
210
+ mock_device = unittest.mock.Mock()
211
+ mock_device.device_path = "/devices/LNXTHERM:00"
212
+ mock_device.sys_path = "/sys/devices/LNXTHERM:00"
213
+ mock_pyudev.list_devices.return_value = [mock_device]
214
+
215
+ # Mock thermal zone files
216
+ mock_read_file.side_effect = [
217
+ "45000", # Current temperature in millidegrees
218
+ "critical", # Trip point 0 type
219
+ "50000", # Trip point 0 temperature in millidegrees
220
+ ]
221
+ mock_os_listdir.return_value = ["trip_point_0_type", "trip_point_0_temp"]
222
+
223
+ # Call the method
224
+ result = self.validator.capture_thermal()
225
+
226
+ # Validate debug messages
227
+ mock_record_debug.assert_any_call("Thermal zones")
228
+ mock_record_debug.assert_any_call("└─LNXTHERM:00")
229
+ mock_record_debug.assert_any_call(" \t temp: 45.0°C")
230
+ mock_record_debug.assert_any_call(" \t critical trip: 50.0°C")
231
+
232
+ # Ensure no prereq was recorded since temp < trip
233
+ mock_record_prereq.assert_not_called()
234
+
235
+ # Stop patches
236
+ patch.stopall()
237
+
238
+ def test_capture_input_wakeup_count(self):
239
+ """Test capture_input_wakeup_count method"""
240
+ # Mock pyudev devices
241
+ mock_pyudev = patch.object(self.validator, "pyudev").start()
242
+ mock_record_debug = patch.object(self.validator.db, "record_debug").start()
243
+ mock_read_file = patch("amd_debug.validator.read_file").start()
244
+ mock_os_path_exists = patch("os.path.exists").start()
245
+
246
+ # Mock input devices
247
+ mock_device = unittest.mock.Mock()
248
+ mock_device.sys_path = "/sys/devices/input0"
249
+ mock_device.parent = None
250
+ mock_pyudev.list_devices.return_value = [mock_device]
251
+
252
+ # Mock wakeup file existence and content
253
+ mock_os_path_exists.side_effect = (
254
+ lambda path: "wakeup" in path or "wakeup_count" in path
255
+ )
256
+ mock_read_file.side_effect = ["5"] # Wakeup count
257
+
258
+ # Set initial wakeup count
259
+ self.validator.wakeup_count = {"/sys/devices/input0": "3"}
260
+
261
+ # Call the method
262
+ self.validator.capture_input_wakeup_count()
263
+
264
+ # Validate debug messages
265
+ mock_record_debug.assert_called_once_with(
266
+ "Woke up from input source /sys/devices/input0 (3->5)", "💤"
267
+ )
268
+
269
+ # Stop patches
270
+ patch.stopall()
271
+
272
+ def test_capture_hw_sleep_suspend_stats(self):
273
+ """Test capture_hw_sleep stats method"""
274
+ # Case 1: Suspend stats file exists and contains valid data
275
+ with patch(
276
+ "os.path.exists", side_effect=lambda path: "suspend_stats" in path
277
+ ), patch("amd_debug.validator.read_file", return_value="1000000"), patch.object(
278
+ self.validator.db, "record_cycle_data"
279
+ ) as mock_record_cycle_data:
280
+ result = self.validator.capture_hw_sleep()
281
+ self.assertTrue(result)
282
+ self.assertEqual(self.validator.hw_sleep_duration, 1.0)
283
+ mock_record_cycle_data.assert_not_called()
284
+
285
+ def test_capture_hw_sleep_smu_fw_info(self):
286
+ """Test capture_hw_sleep smu_fw_info method"""
287
+ # Case 2: Suspend stats file does not exist, fallback to smu_fw_info
288
+ with patch(
289
+ "os.path.exists", side_effect=lambda path: "suspend_stats" not in path
290
+ ), patch(
291
+ "amd_debug.validator.read_file",
292
+ side_effect=[
293
+ "Last S0i3 Status: Success\nTime (in us) in S0i3: 2000000", # smu_fw_info content
294
+ ],
295
+ ), patch.object(
296
+ self.validator.db, "record_cycle_data"
297
+ ) as mock_record_cycle_data:
298
+ result = self.validator.capture_hw_sleep()
299
+ self.assertTrue(result)
300
+ self.assertEqual(self.validator.hw_sleep_duration, 2.0)
301
+ mock_record_cycle_data.assert_not_called()
302
+
303
+ def test_capture_hw_sleep_smu_fw_info_lockdown(self):
304
+ """Test capture_hw_sleep smu_fw_info method while locked down"""
305
+ # Case 3: PermissionError while reading smu_fw_info with lockdown enabled
306
+ self.validator.lockdown = True
307
+ with patch("os.path.exists", return_value=False), patch(
308
+ "amd_debug.validator.read_file", side_effect=PermissionError
309
+ ), patch.object(
310
+ self.validator.db, "record_cycle_data"
311
+ ) as mock_record_cycle_data:
312
+ result = self.validator.capture_hw_sleep()
313
+ self.assertFalse(result)
314
+ mock_record_cycle_data.assert_called_once_with(
315
+ "Unable to gather hardware sleep data with lockdown engaged", "🚦"
316
+ )
317
+
318
+ def test_capture_hw_sleep_smu_fw_info_missing(self):
319
+ """Test capture_hw_sleep smu_fw_info missing method"""
320
+ # Case 4: FileNotFoundError while reading smu_fw_info
321
+ self.validator.lockdown = False
322
+ with patch("os.path.exists", return_value=False), patch(
323
+ "amd_debug.validator.read_file", side_effect=FileNotFoundError
324
+ ), patch.object(self.validator.db, "record_debug") as mock_record:
325
+ result = self.validator.capture_hw_sleep()
326
+ self.assertFalse(result)
327
+ mock_record.assert_called_once_with(
328
+ "HW sleep statistics file /sys/kernel/debug/amd_pmc/smu_fw_info is missing"
329
+ )
330
+
331
+ def test_capture_amdgpu_ips_status(self):
332
+ """Test capture_amdgpu_ips_status method"""
333
+ # Mock pyudev devices
334
+ mock_pyudev = patch.object(self.validator, "pyudev").start()
335
+ mock_record_debug = patch.object(self.validator.db, "record_debug").start()
336
+ mock_read_file = patch("amd_debug.validator.read_file").start()
337
+ mock_os_path_exists = patch("os.path.exists").start()
338
+
339
+ # Mock PCI devices
340
+ mock_device = unittest.mock.Mock()
341
+ mock_device.properties = {
342
+ "PCI_ID": "1002:abcd",
343
+ "PCI_SLOT_NAME": "0000:01:00.0",
344
+ }
345
+ mock_pyudev.list_devices.return_value = [mock_device]
346
+
347
+ # Case 1: IPS status file exists and is readable
348
+ mock_os_path_exists.return_value = True
349
+ mock_read_file.return_value = "IPS Enabled\nIPS Level: 2"
350
+
351
+ self.validator.capture_amdgpu_ips_status()
352
+
353
+ # Validate debug messages
354
+ mock_record_debug.assert_any_call("IPS status")
355
+ mock_record_debug.assert_any_call("│ IPS Enabled")
356
+ mock_record_debug.assert_any_call("└─IPS Level: 2")
357
+
358
+ # Case 2: IPS status file does not exist
359
+ mock_os_path_exists.return_value = False
360
+ self.validator.capture_amdgpu_ips_status()
361
+
362
+ # Case 3: PermissionError while reading IPS status file
363
+ mock_os_path_exists.return_value = True
364
+ mock_read_file.side_effect = PermissionError
365
+ self.validator.lockdown = True
366
+ self.validator.capture_amdgpu_ips_status()
367
+ mock_record_debug.assert_any_call(
368
+ "Unable to gather IPS state data due to kernel lockdown."
369
+ )
370
+
371
+ # Case 4: PermissionError without lockdown
372
+ self.validator.lockdown = False
373
+ self.validator.capture_amdgpu_ips_status()
374
+ mock_record_debug.assert_any_call("Failed to read IPS state data")
375
+
376
+ # Stop patches
377
+ patch.stopall()
378
+
379
+ def test_analyze_kernel_log(self):
380
+ """Test analyze_kernel_log method"""
381
+ # Mock kernel log lines
382
+ mock_kernel_log_lines = [
383
+ "Timekeeping suspended for 123456 us",
384
+ "Successfully transitioned to state lps0 ms entry",
385
+ "Triggering wakeup from IRQ 5",
386
+ "ACPI BIOS Error (bug): Something went wrong",
387
+ "Event logged [IO_PAGE_FAULT device=0000:00:0c.0 domain=0x0000 address=0x7e800000 flags=0x0050]",
388
+ "Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change)",
389
+ ]
390
+
391
+ # Mock kernel log processing
392
+ mock_process_callback = patch.object(
393
+ self.validator.kernel_log, "process_callback"
394
+ ).start()
395
+ mock_process_callback.side_effect = lambda callback: [
396
+ callback(line, 7) for line in mock_kernel_log_lines
397
+ ]
398
+
399
+ # Mock database recording
400
+ mock_record_cycle_data = patch.object(
401
+ self.validator.db, "record_cycle_data"
402
+ ).start()
403
+ mock_record_debug = patch.object(self.validator.db, "record_debug").start()
404
+
405
+ # Call the method
406
+ self.validator.analyze_kernel_log()
407
+
408
+ # Validate recorded cycle data
409
+ mock_record_cycle_data.assert_any_call("Hardware sleep cycle count: 1", "💤")
410
+ mock_record_cycle_data.assert_any_call("ACPI BIOS errors found", "❌")
411
+ mock_record_cycle_data.assert_any_call("Page faults found", "❌")
412
+ mock_record_cycle_data.assert_any_call(
413
+ "Notify devices ['UBTC'] found during suspend", "💤"
414
+ )
415
+
416
+ # Validate recorded debug messages
417
+ mock_record_debug.assert_any_call("Used Microsoft uPEP GUID in LPS0 _DSM")
418
+ mock_record_debug.assert_any_call("Triggering wakeup from IRQ 5", 7)
419
+
420
+ # Stop patches
421
+ patch.stopall()
422
+
423
+ def test_prep(self):
424
+ """Test prep method"""
425
+ with patch("amd_debug.validator.datetime") as mock_datetime, patch.object(
426
+ self.validator.kernel_log, "seek_tail"
427
+ ) as mock_seek_tail, patch.object(
428
+ self.validator.db, "start_cycle"
429
+ ) as mock_start_cycle, patch.object(
430
+ self.validator, "capture_battery"
431
+ ) as mock_capture_battery, patch.object(
432
+ self.validator, "check_gpes"
433
+ ) as mock_check_gpes, patch.object(
434
+ self.validator, "capture_lid"
435
+ ) as mock_capture_lid, patch.object(
436
+ self.validator, "capture_command_line"
437
+ ) as mock_capture_command_line, patch.object(
438
+ self.validator, "capture_wake_sources"
439
+ ) as mock_capture_wake_sources, patch.object(
440
+ self.validator, "capture_running_compositors"
441
+ ) as mock_capture_running_compositors, patch.object(
442
+ self.validator, "capture_power_profile"
443
+ ) as mock_capture_power_profile, patch.object(
444
+ self.validator, "capture_amdgpu_ips_status"
445
+ ) as mock_capture_amdgpu_ips_status, patch.object(
446
+ self.validator, "capture_thermal"
447
+ ) as mock_capture_thermal, patch.object(
448
+ self.validator, "capture_input_wakeup_count"
449
+ ) as mock_capture_input_wakeup_count, patch.object(
450
+ self.validator.acpica, "trace_bios"
451
+ ) as mock_trace_bios, patch.object(
452
+ self.validator.acpica, "trace_notify"
453
+ ) as mock_trace_notify, patch.object(
454
+ self.validator.db, "record_cycle"
455
+ ) as mock_record_cycle:
456
+
457
+ # Mock datetime
458
+ mock_datetime.now.return_value = "mocked_datetime"
459
+
460
+ # Set bios_debug to True and test
461
+ self.validator.bios_debug = True
462
+ self.validator.prep()
463
+ mock_seek_tail.assert_called_once()
464
+ mock_start_cycle.assert_called_once_with("mocked_datetime")
465
+ mock_capture_battery.assert_called_once()
466
+ mock_check_gpes.assert_called_once()
467
+ mock_capture_lid.assert_called_once()
468
+ mock_capture_command_line.assert_called_once()
469
+ mock_capture_wake_sources.assert_called_once()
470
+ mock_capture_running_compositors.assert_called_once()
471
+ mock_capture_power_profile.assert_called_once()
472
+ mock_capture_amdgpu_ips_status.assert_called_once()
473
+ mock_capture_thermal.assert_called_once()
474
+ mock_capture_input_wakeup_count.assert_called_once()
475
+ mock_trace_bios.assert_called_once()
476
+ mock_trace_notify.assert_not_called()
477
+ mock_record_cycle.assert_called_once()
478
+
479
+ # Reset mocks
480
+ mock_seek_tail.reset_mock()
481
+ mock_start_cycle.reset_mock()
482
+ mock_capture_battery.reset_mock()
483
+ mock_check_gpes.reset_mock()
484
+ mock_capture_lid.reset_mock()
485
+ mock_capture_command_line.reset_mock()
486
+ mock_capture_wake_sources.reset_mock()
487
+ mock_capture_running_compositors.reset_mock()
488
+ mock_capture_power_profile.reset_mock()
489
+ mock_capture_amdgpu_ips_status.reset_mock()
490
+ mock_capture_thermal.reset_mock()
491
+ mock_capture_input_wakeup_count.reset_mock()
492
+ mock_trace_bios.reset_mock()
493
+ mock_trace_notify.reset_mock()
494
+ mock_record_cycle.reset_mock()
495
+
496
+ # Set bios_debug to False and test
497
+ self.validator.bios_debug = False
498
+ self.validator.prep()
499
+ mock_seek_tail.assert_called_once()
500
+ mock_start_cycle.assert_called_once_with("mocked_datetime")
501
+ mock_capture_battery.assert_called_once()
502
+ mock_check_gpes.assert_called_once()
503
+ mock_capture_lid.assert_called_once()
504
+ mock_capture_command_line.assert_called_once()
505
+ mock_capture_wake_sources.assert_called_once()
506
+ mock_capture_running_compositors.assert_called_once()
507
+ mock_capture_power_profile.assert_called_once()
508
+ mock_capture_amdgpu_ips_status.assert_called_once()
509
+ mock_capture_thermal.assert_called_once()
510
+ mock_capture_input_wakeup_count.assert_called_once()
511
+ mock_trace_bios.assert_not_called()
512
+ mock_trace_notify.assert_called_once()
513
+ mock_record_cycle.assert_called_once()
514
+
515
+ def test_post(self):
516
+ """Test post method"""
517
+ with patch.object(
518
+ self.validator, "analyze_kernel_log"
519
+ ) as mock_analyze_kernel_log, patch.object(
520
+ self.validator, "capture_wakeup_irq_data"
521
+ ) as mock_capture_wakeup_irq_data, patch.object(
522
+ self.validator, "check_gpes"
523
+ ) as mock_check_gpes, patch.object(
524
+ self.validator, "capture_lid"
525
+ ) as mock_capture_lid, patch.object(
526
+ self.validator, "check_rtc_cmos"
527
+ ) as mock_check_rtc_cmos, patch.object(
528
+ self.validator, "capture_hw_sleep"
529
+ ) as mock_capture_hw_sleep, patch.object(
530
+ self.validator, "capture_battery"
531
+ ) as mock_capture_battery, patch.object(
532
+ self.validator, "capture_amdgpu_ips_status"
533
+ ) as mock_capture_amdgpu_ips_status, patch.object(
534
+ self.validator, "capture_thermal"
535
+ ) as mock_capture_thermal, patch.object(
536
+ self.validator, "capture_input_wakeup_count"
537
+ ) as mock_capture_input_wakeup_count, patch.object(
538
+ self.validator.acpica, "restore"
539
+ ) as mock_acpica_restore, patch.object(
540
+ self.validator.db, "record_cycle"
541
+ ) as mock_record_cycle:
542
+
543
+ # Set mock return values
544
+ mock_analyze_kernel_log.return_value = None
545
+ mock_capture_wakeup_irq_data.return_value = None
546
+ mock_check_gpes.return_value = None
547
+ mock_capture_lid.return_value = None
548
+ mock_check_rtc_cmos.return_value = None
549
+ mock_capture_hw_sleep.return_value = None
550
+ mock_capture_battery.return_value = None
551
+ mock_capture_amdgpu_ips_status.return_value = None
552
+ mock_capture_thermal.return_value = None
553
+ mock_capture_input_wakeup_count.return_value = None
554
+ mock_acpica_restore.return_value = None
555
+
556
+ # Set attributes for record_cycle
557
+ self.validator.requested_duration = 60
558
+ self.validator.active_gpios = ["GPIO1"]
559
+ self.validator.wakeup_irqs = [5]
560
+ self.validator.kernel_duration = 1.5
561
+ self.validator.hw_sleep_duration = 1.0
562
+
563
+ # Call the method
564
+ self.validator.post()
565
+
566
+ # Assert all checks were called
567
+ mock_analyze_kernel_log.assert_called_once()
568
+ mock_capture_wakeup_irq_data.assert_called_once()
569
+ mock_check_gpes.assert_called_once()
570
+ mock_capture_lid.assert_called_once()
571
+ mock_check_rtc_cmos.assert_called_once()
572
+ mock_capture_hw_sleep.assert_called_once()
573
+ mock_capture_battery.assert_called_once()
574
+ mock_capture_amdgpu_ips_status.assert_called_once()
575
+ mock_capture_thermal.assert_called_once()
576
+ mock_capture_input_wakeup_count.assert_called_once()
577
+ mock_acpica_restore.assert_called_once()
578
+
579
+ # Assert record_cycle was called with correct arguments
580
+ mock_record_cycle.assert_called_once_with(
581
+ self.validator.requested_duration,
582
+ self.validator.active_gpios,
583
+ self.validator.wakeup_irqs,
584
+ self.validator.kernel_duration,
585
+ self.validator.hw_sleep_duration,
586
+ )
587
+
588
+ def test_program_wakealarm(self):
589
+ """Test program_wakealarm method"""
590
+ # Mock pyudev devices
591
+ mock_pyudev = patch.object(self.validator, "pyudev").start()
592
+ _mock_record_debug = patch.object(self.validator.db, "record_debug").start()
593
+ mock_print_color = patch("amd_debug.validator.print_color").start()
594
+ mock_open_file = patch("builtins.open", mock_open()).start()
595
+
596
+ # Case 1: RTC device exists
597
+ mock_device = unittest.mock.Mock()
598
+ mock_device.sys_path = "/sys/class/rtc/rtc0"
599
+ mock_pyudev.list_devices.return_value = [mock_device]
600
+ self.validator.requested_duration = 60
601
+
602
+ self.validator.program_wakealarm()
603
+
604
+ # Validate file writes
605
+ mock_open_file.assert_any_call(
606
+ "/sys/class/rtc/rtc0/wakealarm", "w", encoding="utf-8"
607
+ )
608
+ mock_open_file().write.assert_any_call("0")
609
+ mock_open_file().write.assert_any_call("+60\n")
610
+
611
+ # Case 2: No RTC device found
612
+ mock_pyudev.list_devices.return_value = []
613
+ self.validator.program_wakealarm()
614
+
615
+ # Validate print_color call
616
+ mock_print_color.assert_called_once_with(
617
+ "No RTC device found, please manually wake system", "🚦"
618
+ )
619
+
620
+ # Stop patches
621
+ patch.stopall()
622
+
623
+ @patch("amd_debug.validator.SleepReport")
624
+ @patch("amd_debug.validator.print_color")
625
+ def test_report_cycle(self, mock_print_color, mock_sleep_report):
626
+ """Test report_cycle method"""
627
+ # Mock SleepReport instance
628
+ mock_report_instance = mock_sleep_report.return_value
629
+ mock_report_instance.run.return_value = None
630
+
631
+ # Set attributes for the test
632
+ self.validator.last_suspend = "mocked_last_suspend"
633
+ self.validator.display_debug = True
634
+
635
+ # Call the method
636
+ self.validator.report_cycle()
637
+
638
+ # Assert print_color was called with correct arguments
639
+ mock_print_color.assert_called_once_with("Results from last s2idle cycle", "🗣️")
640
+
641
+ # Assert SleepReport was instantiated with correct arguments
642
+ mock_sleep_report.assert_called_once_with(
643
+ since="mocked_last_suspend",
644
+ until="mocked_last_suspend",
645
+ fname=None,
646
+ fmt="stdout",
647
+ tool_debug=True,
648
+ report_debug=False,
649
+ )
650
+
651
+ # Assert run method of SleepReport was called
652
+ mock_report_instance.run.assert_called_once_with(inc_prereq=False)
653
+
654
+ @patch("amd_debug.validator.run_countdown")
655
+ @patch("amd_debug.validator.random.randint")
656
+ @patch("amd_debug.validator.datetime")
657
+ @patch.object(SleepValidator, "prep")
658
+ @patch.object(SleepValidator, "program_wakealarm")
659
+ @patch.object(SleepValidator, "suspend_system")
660
+ @patch.object(SleepValidator, "post")
661
+ @patch.object(SleepValidator, "unlock_session")
662
+ @patch.object(SleepValidator, "report_cycle")
663
+ @patch("amd_debug.validator.print_color")
664
+ def test_run(
665
+ self,
666
+ _mock_print_color,
667
+ mock_report_cycle,
668
+ mock_unlock_session,
669
+ mock_post,
670
+ mock_suspend_system,
671
+ mock_program_wakealarm,
672
+ mock_prep,
673
+ mock_datetime,
674
+ mock_randint,
675
+ mock_run_countdown,
676
+ ):
677
+ """Test the run method"""
678
+ # Mock datetime
679
+ mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0)
680
+
681
+ # Mock suspend_system to return True
682
+ mock_suspend_system.return_value = True
683
+
684
+ # Test case 1: count is 0
685
+ result = self.validator.run(
686
+ duration=10, count=0, wait=5, rand=False, logind=False
687
+ )
688
+ self.assertTrue(result)
689
+
690
+ # Test case 2: logind is True
691
+ self.validator.run(duration=10, count=1, wait=5, rand=False, logind=True)
692
+ self.assertTrue(self.validator.logind)
693
+
694
+ # Test case 3: Randomized test
695
+ mock_randint.side_effect = [7, 3] # Random duration and wait
696
+ self.validator.run(duration=10, count=1, wait=5, rand=True, logind=False)
697
+ mock_randint.assert_any_call(1, 10)
698
+ mock_randint.assert_any_call(1, 5)
699
+ mock_run_countdown.assert_any_call("Suspending system", math.ceil(3 / 2))
700
+ mock_run_countdown.assert_any_call("Collecting data", math.ceil(3 / 2))
701
+ mock_prep.assert_called()
702
+ mock_program_wakealarm.assert_called()
703
+ mock_suspend_system.assert_called()
704
+ mock_post.assert_called()
705
+ mock_report_cycle.assert_called()
706
+ mock_unlock_session.assert_called()
707
+
708
+ # Test case 4: Multiple cycles
709
+ self.validator.run(duration=10, count=2, wait=5, rand=False, logind=False)
710
+ self.assertEqual(mock_prep.call_count, 4) # Includes previous calls
711
+ self.assertEqual(mock_program_wakealarm.call_count, 4)
712
+ self.assertEqual(mock_suspend_system.call_count, 4)
713
+ self.assertEqual(mock_post.call_count, 4)
714
+ self.assertEqual(mock_report_cycle.call_count, 4)
715
+ self.assertEqual(mock_unlock_session.call_count, 3)
716
+
717
+ # Test case 5: suspend_system fails
718
+ mock_suspend_system.return_value = False
719
+ result = self.validator.run(
720
+ duration=10, count=1, wait=5, rand=False, logind=False
721
+ )
722
+ self.assertFalse(result)
723
+ mock_report_cycle.assert_called()