From 3085caaac0d1a108d25108d8995f0ddbc5e59791 Mon Sep 17 00:00:00 2001 From: Junyu Lai Date: Thu, 20 Jun 2024 15:16:25 +0800 Subject: [PATCH] Support VirtualEnv and parameterized tests This commit introduces the use of VirtualEnv to manage test dependencies. Specifically: 1. `PythonVirtualenvPreparer` is invoked before running tests to set up the isolated Python environment. 2. The test is declared as a `MoblyBinaryHostTest` to enable VirtualEnv compatibility, as `PythonBinaryHostTest` lacks this support. 3. The Mobly test runner is invoked manually to accommodate multiple test classes, which is not supported by `mobly.test_runner.main()`. 4. Test classes are refactored to inherit from `mobly.BaseTestClass` for compatibility with the Mobly test runner. This change is a prerequisite for subsequent commits that will install libraries via `pip install`, facilitating isolated dependency management for improved test reliability and maintainability. Also, this commit uses parameterized annotation from absl to rewrite several tests as paremeterized tests. Test: atest NetworkStaticLibHostPythonTests Bug: 335368434 Change-Id: Iffa2f8fa81c3913f3e08cca01b2c009ca959db73 --- staticlibs/tests/unit/Android.bp | 9 +- .../tests/unit/host/python/adb_utils_test.py | 92 +++++++++---------- .../tests/unit/host/python/apf_utils_test.py | 35 ++++--- .../unit/host/python/assert_utils_test.py | 23 +++-- .../tests/unit/host/python/run_tests.py | 18 +++- .../tests/unit/host/python/test_config.xml | 10 +- 6 files changed, 105 insertions(+), 82 deletions(-) diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp index e9f8858d4d..3186033e00 100644 --- a/staticlibs/tests/unit/Android.bp +++ b/staticlibs/tests/unit/Android.bp @@ -76,7 +76,14 @@ python_test_host { test_suites: [ "general-tests", ], + // MoblyBinaryHostTest doesn't support unit_test. test_options: { - unit_test: true, + unit_test: false, + }, + // Needed for applying VirtualEnv. + version: { + py3: { + embedded_launcher: false, + }, }, } diff --git a/staticlibs/tests/unit/host/python/adb_utils_test.py b/staticlibs/tests/unit/host/python/adb_utils_test.py index b75d464fdf..8fcca375f6 100644 --- a/staticlibs/tests/unit/host/python/adb_utils_test.py +++ b/staticlibs/tests/unit/host/python/adb_utils_test.py @@ -12,15 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import MagicMock, patch +from absl.testing import parameterized +from mobly import asserts +from mobly import base_test +from mobly import config_parser from net_tests_utils.host.python import adb_utils from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError -class TestAdbUtils(unittest.TestCase): +class TestAdbUtils(base_test.BaseTestClass, parameterized.TestCase): - def setUp(self): + def __init__(self, configs: config_parser.TestRunConfig): + super().__init__(configs) + + def setup_test(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 @@ -49,25 +55,21 @@ class TestAdbUtils(unittest.TestCase): @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): + with asserts.assert_raises(UnexpectedBehaviorError): adb_utils._set_screen_state(self.mock_ad, True) + @parameterized.parameters( + ("Awake", True), + ("Asleep", False), + ("Dozing", False), + ("SomeOtherState", False), + ) # Declare inputs for state_str and expected_result. @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_screen_state(self, state_str, expected_result, mock_get_value): + mock_get_value.return_value = state_str + asserts.assert_equal( + 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 = ( @@ -76,36 +78,34 @@ class TestAdbUtils(unittest.TestCase): result = adb_utils.get_value_of_key_from_dumpsys( self.mock_ad, "power", "mWakefulness" ) - self.assertEqual(result, "Awake") + asserts.assert_equal(result, "Awake") + @parameterized.parameters( + (True, ["true"]), + (False, ["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 + ) # Declare inputs for expected_state and returned_value @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 - ) + def test_expect_dumpsys_state_with_retry_success( + self, expected_state, returned_value, mock_get_value + ): + 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): + with asserts.assert_raises(UnexpectedBehaviorError): adb_utils.expect_dumpsys_state_with_retry( self.mock_ad, "service", "key", True, 0 ) @@ -116,11 +116,7 @@ class TestAdbUtils(unittest.TestCase): mock_get_value.return_value = None # Expect the function to raise UnexpectedBehaviorError due to the exception - with self.assertRaises(UnexpectedBehaviorError): + with asserts.assert_raises(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/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py index 619609a886..50bc884631 100644 --- a/staticlibs/tests/unit/host/python/apf_utils_test.py +++ b/staticlibs/tests/unit/host/python/apf_utils_test.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import MagicMock, patch +from mobly import asserts +from mobly import base_test +from mobly import config_parser from mobly.controllers.android_device_lib.adb import AdbError from net_tests_utils.host.python.apf_utils import ( PatternNotFoundException, @@ -27,9 +29,12 @@ from net_tests_utils.host.python.apf_utils import ( from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError -class TestApfUtils(unittest.TestCase): +class TestApfUtils(base_test.BaseTestClass): - def setUp(self): + def __init__(self, configs: config_parser.TestRunConfig): + super().__init__(configs) + + def setup_test(self): self.mock_ad = MagicMock() # Mock Android device object @patch("net_tests_utils.host.python.adb_utils.get_dumpsys_for_service") @@ -43,7 +48,7 @@ IpClient.wlan0 COUNTER_NAME2: 456 """ counters = get_apf_counters_from_dumpsys(self.mock_ad, "wlan0") - self.assertEqual(counters, {"COUNTER_NAME1": 123, "COUNTER_NAME2": 456}) + asserts.assert_equal(counters, {"COUNTER_NAME1": 123, "COUNTER_NAME2": 456}) @patch("net_tests_utils.host.python.adb_utils.get_dumpsys_for_service") def test_get_apf_counters_from_dumpsys_exceptions( @@ -63,7 +68,7 @@ IpClient.wlan1 for dumpsys_output in test_cases: mock_get_dumpsys.return_value = dumpsys_output - with self.assertRaises(PatternNotFoundException): + with asserts.assert_raises(PatternNotFoundException): get_apf_counters_from_dumpsys(self.mock_ad, "wlan0") @patch("net_tests_utils.host.python.apf_utils.get_apf_counters_from_dumpsys") @@ -73,9 +78,13 @@ IpClient.wlan1 "COUNTER_NAME1": 123, "COUNTER_NAME2": 456, } - self.assertEqual(get_apf_counter(self.mock_ad, iface, "COUNTER_NAME1"), 123) + asserts.assert_equal( + get_apf_counter(self.mock_ad, iface, "COUNTER_NAME1"), 123 + ) # Not found - self.assertEqual(get_apf_counter(self.mock_ad, iface, "COUNTER_NAME3"), 0) + asserts.assert_equal( + get_apf_counter(self.mock_ad, iface, "COUNTER_NAME3"), 0 + ) @patch("net_tests_utils.host.python.adb_utils.adb_shell") def test_get_hardware_address_success( @@ -86,14 +95,14 @@ IpClient.wlan1 link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff """ mac_address = get_hardware_address(self.mock_ad, "wlan0") - self.assertEqual(mac_address, "72:05:77:82:21:E0") + asserts.assert_equal(mac_address, "72:05:77:82:21:E0") @patch("net_tests_utils.host.python.adb_utils.adb_shell") def test_get_hardware_address_not_found( self, mock_adb_shell: MagicMock ) -> None: mock_adb_shell.return_value = "Some output without MAC address" - with self.assertRaises(PatternNotFoundException): + with asserts.assert_raises(PatternNotFoundException): get_hardware_address(self.mock_ad, "wlan0") @patch("net_tests_utils.host.python.apf_utils.get_hardware_address") @@ -133,7 +142,7 @@ IpClient.wlan1 mock_adb_shell.return_value = ( # Unexpected command output "Any Unexpected Output" ) - with self.assertRaises(UnexpectedBehaviorError): + with asserts.assert_raises(UnexpectedBehaviorError): send_raw_packet_downstream( self.mock_ad, "BEEF", "eth0", "1234567890AB", "AABBCCDDEEFF" ) @@ -145,11 +154,7 @@ IpClient.wlan1 mock_adb_shell.side_effect = AdbError( cmd="", stdout="Unknown command", stderr="", ret_code=3 ) - with self.assertRaises(UnsupportedOperationException): + with asserts.assert_raises(UnsupportedOperationException): send_raw_packet_downstream( self.mock_ad, "BEEF", "eth0", "1234567890AB", "AABBCCDDEEFF" ) - - -if __name__ == "__main__": - unittest.main() diff --git a/staticlibs/tests/unit/host/python/assert_utils_test.py b/staticlibs/tests/unit/host/python/assert_utils_test.py index b970d14c53..7a33373037 100644 --- a/staticlibs/tests/unit/host/python/assert_utils_test.py +++ b/staticlibs/tests/unit/host/python/assert_utils_test.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +from mobly import asserts +from mobly import base_test from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError, expect_with_retry -class TestAssertUtils(unittest.TestCase): +class TestAssertUtils(base_test.BaseTestClass): def test_predicate_succeed(self): """Test when the predicate becomes True within retries.""" @@ -28,12 +29,12 @@ class TestAssertUtils(unittest.TestCase): return call_count > 2 # True on the third call expect_with_retry(predicate, max_retries=5, retry_interval_sec=0) - self.assertEqual(call_count, 3) # Ensure it was called exactly 3 times + asserts.assert_equal(call_count, 3) # Ensure it was called exactly 3 times def test_predicate_failed(self): """Test when the predicate never becomes True.""" - with self.assertRaises(UnexpectedBehaviorError): + with asserts.assert_raises(UnexpectedBehaviorError): expect_with_retry( predicate=lambda: False, max_retries=3, retry_interval_sec=0 ) @@ -52,7 +53,9 @@ class TestAssertUtils(unittest.TestCase): max_retries=5, retry_interval_sec=0, ) - self.assertFalse(retry_action_called) # Assert retry_action was NOT called + asserts.assert_false( + retry_action_called, "retry_action called." + ) # Assert retry_action was NOT called def test_retry_action_not_called_failed(self): """Test that the retry_action is not called if the max_retries is reached.""" @@ -62,14 +65,16 @@ class TestAssertUtils(unittest.TestCase): nonlocal retry_action_called retry_action_called = True - with self.assertRaises(UnexpectedBehaviorError): + with asserts.assert_raises(UnexpectedBehaviorError): expect_with_retry( predicate=lambda: False, retry_action=retry_action, max_retries=1, retry_interval_sec=0, ) - self.assertFalse(retry_action_called) # Assert retry_action was NOT called + asserts.assert_false( + retry_action_called, "retry_action called." + ) # Assert retry_action was NOT called def test_retry_action_called(self): """Test that the retry_action is executed when provided.""" @@ -79,11 +84,11 @@ class TestAssertUtils(unittest.TestCase): nonlocal retry_action_called retry_action_called = True - with self.assertRaises(UnexpectedBehaviorError): + with asserts.assert_raises(UnexpectedBehaviorError): expect_with_retry( predicate=lambda: False, retry_action=retry_action, max_retries=2, retry_interval_sec=0, ) - self.assertTrue(retry_action_called) + asserts.assert_true(retry_action_called, "retry_action not called.") diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py index c44f1a80f6..fa6a310293 100644 --- a/staticlibs/tests/unit/host/python/run_tests.py +++ b/staticlibs/tests/unit/host/python/run_tests.py @@ -14,14 +14,22 @@ """Main entrypoint for all of unittest.""" -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. +import sys from host.python.adb_utils_test import TestAdbUtils from host.python.apf_utils_test import TestApfUtils from host.python.assert_utils_test import TestAssertUtils +from mobly import suite_runner if __name__ == "__main__": - unittest.main() + # For MoblyBinaryHostTest, this entry point will be called twice: + # 1. List tests. + # -- --list_tests + # 2. Run tests. + # -- --config= --device_serial= --log_path= + # Strip the "--" since suite runner doesn't recognize it. + sys.argv.pop(1) + # TODO: make the tests can be executed without manually list classes. + suite_runner.run_suite( + [TestAssertUtils, TestAdbUtils, TestApfUtils], sys.argv + ) diff --git a/staticlibs/tests/unit/host/python/test_config.xml b/staticlibs/tests/unit/host/python/test_config.xml index 26ee9e2a82..d3b200a69c 100644 --- a/staticlibs/tests/unit/host/python/test_config.xml +++ b/staticlibs/tests/unit/host/python/test_config.xml @@ -14,9 +14,11 @@ limitations under the License. --> - - -