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:
parent
48fea4f49e
commit
3085caaac0
6 changed files with 106 additions and 83 deletions
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue