diff --git a/staticlibs/tests/unit/host/python/adb_utils_test.py b/staticlibs/tests/unit/host/python/adb_utils_test.py new file mode 100644 index 0000000000..b75d464fdf --- /dev/null +++ b/staticlibs/tests/unit/host/python/adb_utils_test.py @@ -0,0 +1,126 @@ +# Copyright (C) 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import MagicMock, patch +from net_tests_utils.host.python import adb_utils +from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError + + +class TestAdbUtils(unittest.TestCase): + + def setUp(self): + self.mock_ad = MagicMock() # Mock Android device object + self.mock_ad.log = MagicMock() + self.mock_ad.adb.shell.return_value = b"" # Default empty return for shell + + @patch( + "net_tests_utils.host.python.adb_utils.expect_dumpsys_state_with_retry" + ) + @patch("net_tests_utils.host.python.adb_utils._set_screen_state") + def test_set_doze_mode_enable( + self, mock_set_screen_state, mock_expect_dumpsys_state + ): + adb_utils.set_doze_mode(self.mock_ad, True) + mock_set_screen_state.assert_called_once_with(self.mock_ad, False) + + @patch( + "net_tests_utils.host.python.adb_utils.expect_dumpsys_state_with_retry" + ) + def test_set_doze_mode_disable(self, mock_expect_dumpsys_state): + adb_utils.set_doze_mode(self.mock_ad, False) + + @patch("net_tests_utils.host.python.adb_utils._get_screen_state") + def test_set_screen_state_success(self, mock_get_screen_state): + mock_get_screen_state.side_effect = [False, True] # Simulate toggle + adb_utils._set_screen_state(self.mock_ad, True) + + @patch("net_tests_utils.host.python.adb_utils._get_screen_state") + def test_set_screen_state_failure(self, mock_get_screen_state): + mock_get_screen_state.return_value = False # State doesn't change + with self.assertRaises(UnexpectedBehaviorError): + adb_utils._set_screen_state(self.mock_ad, True) + + @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys") + def test_get_screen_state(self, mock_get_value): + # Test cases for different return values of get_value_of_key_from_dumpsys + # TODO: Make it parameterized. + test_cases = [ + ("Awake", True), + ("Asleep", False), + ("Dozing", False), + ("SomeOtherState", False), + ] + + for state_str, expected_result in test_cases: + mock_get_value.return_value = state_str + self.assertEqual( + adb_utils._get_screen_state(self.mock_ad), expected_result + ) + + def test_get_value_of_key_from_dumpsys(self): + self.mock_ad.adb.shell.return_value = ( + b"mWakefulness=Awake\nmOtherKey=SomeValue" + ) + result = adb_utils.get_value_of_key_from_dumpsys( + self.mock_ad, "power", "mWakefulness" + ) + self.assertEqual(result, "Awake") + + @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys") + def test_expect_dumpsys_state_with_retry_success(self, mock_get_value): + # Test cases for different combinations of expected_state and get_value return value + # TODO: Make it parameterized. + test_cases = [ + (True, ["true"]), # Expect True, get True + (False, ["false"]), # Expect False, get False + ( + True, + ["false", "true"], + ), # Expect True, get False which is unexpected, then get True + ( + False, + ["true", "false"], + ), # Expect False, get True which is unexpected, then get False + ] + + for expected_state, returned_value in test_cases: + mock_get_value.side_effect = returned_value + # Verify the method returns and does not throw. + adb_utils.expect_dumpsys_state_with_retry( + self.mock_ad, "service", "key", expected_state, 0 + ) + + @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys") + def test_expect_dumpsys_state_with_retry_failure(self, mock_get_value): + mock_get_value.return_value = "false" + with self.assertRaises(UnexpectedBehaviorError): + adb_utils.expect_dumpsys_state_with_retry( + self.mock_ad, "service", "key", True, 0 + ) + + @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys") + def test_expect_dumpsys_state_with_retry_not_found(self, mock_get_value): + # Simulate the get_value_of_key_from_dumpsys cannot find the give key. + mock_get_value.return_value = None + + # Expect the function to raise UnexpectedBehaviorError due to the exception + with self.assertRaises(UnexpectedBehaviorError): + adb_utils.expect_dumpsys_state_with_retry( + self.mock_ad, "service", "key", True + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py index 8059568850..7d58c6e2c9 100644 --- a/staticlibs/tests/unit/host/python/run_tests.py +++ b/staticlibs/tests/unit/host/python/run_tests.py @@ -18,6 +18,7 @@ import unittest # Import all unittest classes here, so it can be discovered by unittest module. # TODO: make the tests can be executed without manually import classes. +from host.python.adb_utils_test import TestAdbUtils from host.python.assert_utils_test import TestAssertUtils diff --git a/staticlibs/testutils/host/python/adb_utils.py b/staticlibs/testutils/host/python/adb_utils.py new file mode 100644 index 0000000000..13c06464fd --- /dev/null +++ b/staticlibs/testutils/host/python/adb_utils.py @@ -0,0 +1,118 @@ +# Copyright (C) 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from mobly.controllers import android_device +from net_tests_utils.host.python import assert_utils + +BYTE_DECODE_UTF_8 = "utf-8" + + +def set_doze_mode(ad: android_device.AndroidDevice, enable: bool) -> None: + if enable: + adb_shell(ad, "cmd battery unplug") + expect_dumpsys_state_with_retry( + ad, "deviceidle", key="mCharging", expected_state=False + ) + _set_screen_state(ad, False) + adb_shell(ad, "dumpsys deviceidle enable deep") + expect_dumpsys_state_with_retry( + ad, "deviceidle", key="mDeepEnabled", expected_state=True + ) + adb_shell(ad, "dumpsys deviceidle force-idle deep") + expect_dumpsys_state_with_retry( + ad, "deviceidle", key="mForceIdle", expected_state=True + ) + else: + adb_shell(ad, "cmd battery reset") + expect_dumpsys_state_with_retry( + ad, "deviceidle", key="mCharging", expected_state=True + ) + adb_shell(ad, "dumpsys deviceidle unforce") + expect_dumpsys_state_with_retry( + ad, "deviceidle", key="mForceIdle", expected_state=False + ) + + +def _set_screen_state( + ad: android_device.AndroidDevice, target_state: bool +) -> None: + assert_utils.expect_with_retry( + predicate=lambda: _get_screen_state(ad) == target_state, + retry_action=lambda: adb_shell( + ad, "input keyevent KEYCODE_POWER" + ), # Toggle power key again when retry. + ) + + +def _get_screen_state(ad: android_device.AndroidDevice) -> bool: + return get_value_of_key_from_dumpsys(ad, "power", "mWakefulness") == "Awake" + + +def get_value_of_key_from_dumpsys( + ad: android_device.AndroidDevice, service: str, key: str +) -> str: + output = get_dumpsys_for_service(ad, service) + # Search for key=value pattern from the dumpsys output. + # e.g. mWakefulness=Awake + pattern = rf"{key}=(.*)" + # Only look for the first occurrence. + match = re.search(pattern, output) + if match: + ad.log.debug( + "Getting key-value from dumpsys: " + key + "=" + match.group(1) + ) + return match.group(1) + else: + return None + + +def expect_dumpsys_state_with_retry( + ad: android_device.AndroidDevice, + service: str, + key: str, + expected_state: bool, + retry_interval_sec: int = 1, +) -> None: + def predicate(): + value = get_value_of_key_from_dumpsys(ad, service, key) + if value is None: + return False + return value.lower() == str(expected_state).lower() + + assert_utils.expect_with_retry( + predicate=predicate, + retry_interval_sec=retry_interval_sec, + ) + + +def get_dumpsys_for_service( + ad: android_device.AndroidDevice, service: str +) -> str: + return adb_shell(ad, "dumpsys " + service) + + +def adb_shell(ad: android_device.AndroidDevice, shell_cmd: str) -> str: + """Runs adb shell command. + + Args: + ad: Android device object. + shell_cmd: string of list of strings, adb shell command. + + Returns: + string, replies from adb shell command. + """ + ad.log.debug("Executing adb shell %s", shell_cmd) + data = ad.adb.shell(shell_cmd) + return data.decode(BYTE_DECODE_UTF_8).strip()