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
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index e9f8858..3186033 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -76,7 +76,14 @@
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 b75d464..8fcca37 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 @@
@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 @@
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 @@
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 619609a..50bc884 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.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 @@
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 @@
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 @@
"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 @@
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 @@
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 @@
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 b970d14..7a33373 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 @@
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 @@
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 @@
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 @@
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 c44f1a8..fa6a310 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.
+ # <mobly-par-file-name> -- --list_tests
+ # 2. Run tests.
+ # <mobly-par-file-name> -- --config=<yaml-path> --device_serial=<device-serial> --log_path=<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 26ee9e2..d3b200a 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.
-->
<configuration description="Config for NetworkStaticLibHostPythonTests">
- <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
- <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest">
- <option name="par-file-name" value="NetworkStaticLibHostPythonTests" />
- <option name="test-timeout" value="3m" />
+ <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
+ <option name="dep-module" value="absl-py" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest" >
+ <option name="mobly-par-file-name" value="NetworkStaticLibHostPythonTests" />
+ <option name="mobly-test-timeout" value="3m" />
</test>
</configuration>