diff --git a/checkbox-support/checkbox_support/scripts/fwts_test.py b/checkbox-support/checkbox_support/scripts/fwts_test.py index e18686397b..2322897300 100644 --- a/checkbox-support/checkbox_support/scripts/fwts_test.py +++ b/checkbox-support/checkbox_support/scripts/fwts_test.py @@ -267,7 +267,7 @@ def print_log(logfile): print("WARNING: Found bad char in " + logfile) -def main(): +def parse_arguments(args): description_text = "Tests the system BIOS using the Firmware Test Suite" epilog_text = ( "To perform sleep testing, you will need at least some of " @@ -388,7 +388,12 @@ def main(): action="store_true", help=("List all Server Certification concerned tests " "in fwts"), ) - args = parser.parse_args() + args = parser.parse_args(args) + return args + + +def main(args=sys.argv[1:]): + args = parse_arguments(args) tests = [] requested_tests = [] diff --git a/providers/base/bin/suspend.sh b/providers/base/bin/suspend.sh deleted file mode 100755 index 52df0212a4..0000000000 --- a/providers/base/bin/suspend.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# FWTS s3 only support x86_64 and i386 cpu -# ref: https://github.com/ColinIanKing/fwts/blob/master/src/acpi/s3/s3.c#L24 -# So we need to seperate two kind of situation (FWTS_S3 supported or not) - -architecture=$(uname -m) -if [ "$architecture" = "x86_64" ] || [ "$architecture" = "i386" ]; then - export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SNAP/usr/lib/fwts" - set -o pipefail - checkbox-support-fwts_test -f none -s s3 --s3-device-check --s3-device-check-delay="${STRESS_S3_WAIT_DELAY:-45}" --s3-sleep-delay="${STRESS_S3_SLEEP_DELAY:-30}" - rm /tmp/fwts_results.log -else - rtcwake -v -d "${RTC_DEVICE_FILE:-/dev/rtc0}" -m no -s "${STRESS_S3_SLEEP_DELAY:-30}" && systemctl suspend || exit 1 -fi - diff --git a/providers/base/bin/suspend_trigger.py b/providers/base/bin/suspend_trigger.py new file mode 100755 index 0000000000..376a65d35a --- /dev/null +++ b/providers/base/bin/suspend_trigger.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import argparse +import platform +import subprocess +import sys +import time + +from checkbox_support.scripts import fwts_test + + +def main(args=sys.argv[1:]): + parser = argparse.ArgumentParser() + parser.add_argument( + "--wait", + type=int, + help="Time (in seconds) to wait before triggering suspend.", + ) + parser.add_argument( + "--check-delay", + type=int, + help=( + "Time (in seconds) for FWTS to wait before checking the device " + "after resuming." + ), + default=45, + ) + parser.add_argument( + "--sleep-delay", + type=int, + help="Time (in seconds) to sleep before resuming the device.", + default=30, + ) + parser.add_argument( + "--rtc-device", + help="Real Time Clock device to use (for ARM devices only)", + default="/dev/rtc0", + ) + args = parser.parse_args(args) + if args.wait: + print("Waiting for {} seconds...".format(args.wait)) + time.sleep(args.wait) + if platform.machine() in ["i386", "x86_64"]: + print("Running FWTS to trigger suspend...") + fwts_args = [ + "-f", + "none", + "-s", + "s3", + "--s3-device-check", + "--s3-device-check-delay", + str(args.check_delay), + "--s3-sleep-delay", + str(args.sleep_delay), + ] + fwts_test.main(fwts_args) + else: + rtcwake_cmd = [ + "rtcwake", + "--verbose", + "--device", + args.rtc_device, + "--mode", + "no", + "--seconds", + str(args.sleep_delay), + ] + suspend_cmd = ["systemctl", "suspend"] + print("Running: {}".format(" ".join(rtcwake_cmd))) + subprocess.check_call(rtcwake_cmd) + print( + "Running: {} to suspend the system".format(" ".join(suspend_cmd)) + ) + subprocess.check_call(suspend_cmd) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/providers/base/tests/test_suspend_trigger.py b/providers/base/tests/test_suspend_trigger.py new file mode 100644 index 0000000000..06f34753b2 --- /dev/null +++ b/providers/base/tests/test_suspend_trigger.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +import unittest +from unittest.mock import patch, call +import subprocess + +import suspend_trigger + + +@patch("suspend_trigger.fwts_test") +@patch("suspend_trigger.time.sleep") +@patch("suspend_trigger.platform.machine") +class TestSuspendTriggerFWTS(unittest.TestCase): + + def test_wait_argument(self, mock_machine, mock_sleep, mock_fwts_test): + """ + Tests if the --wait argument correctly calls time.sleep. + """ + mock_machine.return_value = "x86_64" + suspend_trigger.main(["--wait", "15"]) + mock_sleep.assert_called_once_with(15) + # Verify that the rest of the script (fwts path) also runs + self.assertTrue(mock_fwts_test.main.called) + + def test_fwts_path_on_x86_64_with_arguments( + self, mock_machine, mock_sleep, mock_fwts_test + ): + """ + Tests the FWTS code path on x86_64 architecture with custom arguments. + """ + mock_machine.return_value = "x86_64" + + suspend_trigger.main(["--sleep-delay", "22", "--check-delay", "55"]) + + mock_sleep.assert_not_called() + self.assertTrue(mock_machine.called) + expected_fwts_args = [ + "-f", + "none", + "-s", + "s3", + "--s3-device-check", + "--s3-device-check-delay", + "55", + "--s3-sleep-delay", + "22", + ] + mock_fwts_test.main.assert_called_once_with(expected_fwts_args) + + def test_fwts_path_on_i386_with_defaults( + self, mock_machine, mock_sleep, mock_fwts_test + ): + """ + Tests the FWTS code path on i386 with no arguments. + """ + # Mock os.getenv to return the default value passed to it in the script + mock_machine.return_value = "i386" + + suspend_trigger.main([]) + + mock_sleep.assert_not_called() + expected_fwts_args = [ + "-f", + "none", + "-s", + "s3", + "--s3-device-check", + "--s3-device-check-delay", + "45", # Default from script + "--s3-sleep-delay", + "30", # Default from script + ] + mock_fwts_test.main.assert_called_once_with(expected_fwts_args) + + +@patch("suspend_trigger.fwts_test") +@patch("suspend_trigger.subprocess.check_call") +@patch("suspend_trigger.platform.machine") +class TestSuspendTriggerRTCWake(unittest.TestCase): + def test_rtcwake_path_success_with_args( + self, mock_machine, mock_check_call, mock_fwts_test + ): + """ + Tests the rtcwake/systemctl path on aarch64 with custom arguments. + """ + mock_machine.return_value = "aarch64" + + suspend_trigger.main( + ["--sleep-delay", "25", "--rtc-device", "/dev/my_rtc"] + ) + + self.assertFalse(mock_fwts_test.main.called) + expected_rtcwake_cmd = [ + "rtcwake", + "--verbose", + "--device", + "/dev/my_rtc", + "--mode", + "no", + "--seconds", + "25", + ] + expected_suspend_cmd = ["systemctl", "suspend"] + subprocess_calls = [ + call(expected_rtcwake_cmd), + call(expected_suspend_cmd), + ] + mock_check_call.assert_has_calls(subprocess_calls) + self.assertEqual(mock_check_call.call_count, 2) + + def test_rtcwake_path_with_defaults( + self, mock_machine, mock_check_call, mock_fwts_test + ): + """ + Tests the rtcwake/systemctl path without any argument. + """ + mock_machine.return_value = "riscv64" + + suspend_trigger.main([]) + + expected_rtcwake_cmd = [ + "rtcwake", + "--verbose", + "--device", + "/dev/rtc0", + "--mode", + "no", + "--seconds", + "30", + ] + expected_suspend_cmd = ["systemctl", "suspend"] + subprocess_calls = [ + call(expected_rtcwake_cmd), + call(expected_suspend_cmd), + ] + mock_check_call.assert_has_calls(subprocess_calls) + + def test_rtcwake_command_failure( + self, mock_machine, mock_check_call, mock_fwts_test + ): + """ + Tests the case where the rtcwake command fails. + """ + mock_machine.return_value = "aarch64" + # Simulate a command failure + error = subprocess.CalledProcessError( + returncode=1, cmd="rtcwake", output="Error from rtcwake" + ) + mock_check_call.side_effect = error + + # The script should propagate the exception + with self.assertRaises(subprocess.CalledProcessError): + suspend_trigger.main([]) + + # Verify that only the first command (rtcwake) was attempted + self.assertTrue(mock_check_call.called) + self.assertIn("rtcwake", mock_check_call.call_args[0][0]) + + def test_suspend_command_failure( + self, mock_machine, mock_check_call, mock_fwts_test + ): + """ + Tests the case where the systemctl suspend command fails. + """ + mock_machine.return_value = "aarch64" + suspend_error = subprocess.CalledProcessError( + returncode=1, cmd="systemctl suspend", output="Error from suspend" + ) + # The first call (rtcwake) succeeds, the second (suspend) fails. + mock_check_call.side_effect = [None, suspend_error] + + with self.assertRaises(subprocess.CalledProcessError): + suspend_trigger.main([]) + + # Verify both commands were attempted + self.assertEqual(mock_check_call.call_count, 2) + self.assertIn("rtcwake", mock_check_call.call_args_list[0][0][0]) + self.assertIn("systemctl", mock_check_call.call_args_list[1][0][0]) diff --git a/providers/base/units/stress/suspend_cycles_reboot.pxu b/providers/base/units/stress/suspend_cycles_reboot.pxu index c754c7a7d4..921887bbfc 100644 --- a/providers/base/units/stress/suspend_cycles_reboot.pxu +++ b/providers/base/units/stress/suspend_cycles_reboot.pxu @@ -76,11 +76,11 @@ requires: sleep.mem == 'supported' rtc.state == 'supported' estimated_duration: 75.0 -environ: PLAINBOX_SESSION_SHARE STRESS_S3_SLEEP_DELAY STRESS_S3_WAIT_DELAY LD_LIBRARY_PATH RTC_DEVICE_FILE +environ: PLAINBOX_SESSION_SHARE STRESS_S3_INIT_DELAY STRESS_S3_SLEEP_DELAY STRESS_S3_WAIT_DELAY LD_LIBRARY_PATH RTC_DEVICE_FILE user: root command: echo "Current boot ID is: $(tr -d - < /proc/sys/kernel/random/boot_id)" - suspend.sh 2>&1 | tee -a "$PLAINBOX_SESSION_SHARE"/suspend_cycles_with_reboot_total.log + suspend_trigger.py --wait "${STRESS_S3_INIT_DELAY:-120}" --check-delay "${STRESS_S3_WAIT_DELAY:-45}" --sleep-delay "${STRESS_S3_SLEEP_DELAY:-30}" --rtc-device "${RTC_DEVICE_FILE:-/dev/rtc0}" 2>&1 | tee -a "$PLAINBOX_SESSION_SHARE"/suspend_cycles_with_reboot_total.log summary: Suspend and resume device (suspend cycle 1, reboot cycle 1) _purpose: Suspend and resume device (suspend cycle 1, reboot cycle 1) @@ -101,12 +101,12 @@ requires: sleep.mem == 'supported' rtc.state == 'supported' estimated_duration: 75.0 -environ: PLAINBOX_SESSION_SHARE STRESS_S3_SLEEP_DELAY STRESS_S3_WAIT_DELAY LD_LIBRARY_PATH RTC_DEVICE_FILE +environ: PLAINBOX_SESSION_SHARE STRESS_S3_INIT_DELAY STRESS_S3_SLEEP_DELAY STRESS_S3_WAIT_DELAY LD_LIBRARY_PATH RTC_DEVICE_FILE after: stress-tests/suspend_cycles_reboot{{suspend_reboot_previous}} user: root command: echo "Current boot ID is: $(tr -d - < /proc/sys/kernel/random/boot_id)" - suspend.sh 2>&1 | tee -a "$PLAINBOX_SESSION_SHARE"/suspend_cycles_with_reboot_total.log + suspend_trigger.py --wait "${STRESS_S3_INIT_DELAY:-120}" --check-delay "${STRESS_S3_WAIT_DELAY:-45}" --sleep-delay "${STRESS_S3_SLEEP_DELAY:-30}" --rtc-device "${RTC_DEVICE_FILE:-/dev/rtc0}" 2>&1 | tee -a "$PLAINBOX_SESSION_SHARE"/suspend_cycles_with_reboot_total.log summary: Suspend and resume device (suspend cycle 1, reboot cycle {{suspend_reboot_id}}) @@ -125,12 +125,12 @@ requires: sleep.mem == 'supported' rtc.state == 'supported' estimated_duration: 75.0 -environ: PLAINBOX_SESSION_SHARE STRESS_S3_SLEEP_DELAY STRESS_S3_WAIT_DELAY LD_LIBRARY_PATH +environ: PLAINBOX_SESSION_SHARE STRESS_S3_INIT_DELAY STRESS_S3_SLEEP_DELAY STRESS_S3_WAIT_DELAY LD_LIBRARY_PATH RTC_DEVICE_FILE after: stress-tests/suspend_cycles_{{suspend_id_previous}}_reboot{{suspend_reboot_id}} user: root command: echo "Current boot ID is: $(tr -d - < /proc/sys/kernel/random/boot_id)" - suspend.sh 2>&1 | tee -a "$PLAINBOX_SESSION_SHARE"/suspend_cycles_with_reboot_total.log + suspend_trigger.py --check-delay "${STRESS_S3_WAIT_DELAY:-45}" --sleep-delay "${STRESS_S3_SLEEP_DELAY:-30}" --rtc-device "${RTC_DEVICE_FILE:-/dev/rtc0}" 2>&1 | tee -a "$PLAINBOX_SESSION_SHARE"/suspend_cycles_with_reboot_total.log summary: Suspend and resume device (suspend cycle {{suspend_id}}, reboot cycle {{suspend_reboot_id}}) _summary: