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
This commit is contained in:
Junyu Lai 2024-06-20 15:16:25 +08:00
commit 3085caaac0
6 changed files with 106 additions and 83 deletions

View file

@ -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,
},
},
}

View file

@ -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()

View file

@ -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()

View file

@ -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.")

View file

@ -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
)

View file

@ -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>