Merge "Create the first Nearby Mainline e2e test: SeekerDiscoverProviderTest." into tm-dev
diff --git a/nearby/tests/multidevices/host/Android.bp b/nearby/tests/multidevices/host/Android.bp
new file mode 100644
index 0000000..b0adfba
--- /dev/null
+++ b/nearby/tests/multidevices/host/Android.bp
@@ -0,0 +1,42 @@
+// Copyright (C) 2022 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.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Run the tests: atest -v CtsSeekerDiscoverProviderTest
+python_test_host {
+ name: "CtsSeekerDiscoverProviderTest",
+ main: "seeker_discover_provider_test.py",
+ srcs: ["seeker_discover_provider_test.py"],
+ libs: ["NearbyMultiDevicesHostHelper"],
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ test_options: {
+ unit_test: false,
+ },
+ data: [
+ // Package the snippet with the Mobly test
+ ":NearbyMultiDevicesClientsSnippets",
+ ],
+}
+
+python_library_host {
+ name: "NearbyMultiDevicesHostHelper",
+ srcs: ["*.py"],
+ exclude_srcs: ["*_test.py"],
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/host/AndroidTest.xml b/nearby/tests/multidevices/host/AndroidTest.xml
new file mode 100644
index 0000000..cca0826
--- /dev/null
+++ b/nearby/tests/multidevices/host/AndroidTest.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<configuration description="Config for CTS seeker scan provider test">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="wifi" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
+
+ <device name="device1">
+ <!-- For coverage to work, the APK should not be uninstalled until after coverage is pulled.
+ So it's a lot easier to install APKs outside the python code.
+ -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NearbyMultiDevicesClientsSnippets.apk" />
+ <option name="check-min-sdk" value="true" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
+ <!-- Any python dependencies can be specified and will be installed with pip -->
+ <option name="dep-module" value="mobly" />
+ <option name="dep-module" value="retry" />
+ </target_preparer>
+ </device>
+ <device name="device2">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NearbyMultiDevicesClientsSnippets.apk" />
+ <option name="check-min-sdk" value="true" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+ </device>
+
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
+ <!-- The mobly-par-file-name should match the module name -->
+ <option name="mobly-par-file-name" value="CtsSeekerDiscoverProviderTest" />
+ <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+ <option name="mobly-test-timeout" value="60000" />
+ </test>
+</configuration>
+
diff --git a/nearby/tests/multidevices/host/event_helper.py b/nearby/tests/multidevices/host/event_helper.py
new file mode 100644
index 0000000..a642246
--- /dev/null
+++ b/nearby/tests/multidevices/host/event_helper.py
@@ -0,0 +1,55 @@
+"""This is a shared library to help handling Mobly event waiting logic."""
+
+import time
+from typing import Callable
+
+from mobly.controllers.android_device_lib import callback_handler
+from mobly.controllers.android_device_lib import snippet_event
+
+# Abbreviations for common use type
+CallbackHandler = callback_handler.CallbackHandler
+SnippetEvent = snippet_event.SnippetEvent
+
+# Type definition for the callback functions to make code formatted nicely
+OnReceivedCallback = Callable[[SnippetEvent, int], bool]
+OnWaitingCallback = Callable[[int], None]
+OnMissedCallback = Callable[[], None]
+
+
+def wait_callback_event(callback_event_handler: CallbackHandler,
+ event_name: str, timeout_seconds: int,
+ on_received: OnReceivedCallback,
+ on_waiting: OnWaitingCallback,
+ on_missed: OnMissedCallback) -> None:
+ """Waits until the matched event has been received or timeout.
+
+ Here we keep waitAndGet for event callback from EventSnippet.
+ We loop until over timeout_seconds instead of directly
+ waitAndGet(timeout=teardown_timeout_seconds). Because there is
+ MAX_TIMEOUT limitation in callback_handler of Mobly.
+
+ Args:
+ callback_event_handler: Mobly callback events handler.
+ event_name: the specific name of the event to wait.
+ timeout_seconds: the number of seconds to wait before giving up.
+ on_received: calls when event received, return false to keep waiting.
+ on_waiting: calls when waitAndGet timeout.
+ on_missed: calls when giving up.
+ """
+ start_time = time.perf_counter()
+ deadline = start_time + timeout_seconds
+ while time.perf_counter() < deadline:
+ remaining_time_sec = min(callback_handler.DEFAULT_TIMEOUT,
+ deadline - time.perf_counter())
+ try:
+ event = callback_event_handler.waitAndGet(
+ event_name, timeout=remaining_time_sec)
+ except callback_handler.TimeoutError:
+ elapsed_time = int(time.perf_counter() - start_time)
+ on_waiting(elapsed_time)
+ else:
+ elapsed_time = int(time.perf_counter() - start_time)
+ if on_received(event, elapsed_time):
+ break
+ else:
+ on_missed()
diff --git a/nearby/tests/multidevices/host/fast_pair_provider_simulator.py b/nearby/tests/multidevices/host/fast_pair_provider_simulator.py
new file mode 100644
index 0000000..1f62dfb
--- /dev/null
+++ b/nearby/tests/multidevices/host/fast_pair_provider_simulator.py
@@ -0,0 +1,133 @@
+"""Fast Pair provider simulator role."""
+
+from mobly import asserts
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import snippet_event
+import retry
+
+import event_helper
+
+# The package name of the provider simulator snippet.
+FP_PROVIDER_SIMULATOR_SNIPPETS_PACKAGE = 'android.nearby.multidevices'
+
+# Events reported from the provider simulator snippet.
+ON_SCAN_MODE_CHANGE_EVENT = 'onScanModeChange'
+ON_ADVERTISING_CHANGE_EVENT = 'onAdvertisingChange'
+
+# Target scan mode.
+DISCOVERABLE_MODE = 'DISCOVERABLE'
+
+# Abbreviations for common use type.
+AndroidDevice = android_device.AndroidDevice
+SnippetEvent = snippet_event.SnippetEvent
+wait_for_event = event_helper.wait_callback_event
+
+
+class FastPairProviderSimulator:
+ """A proxy for provider simulator snippet on the device."""
+
+ def __init__(self, ad: AndroidDevice) -> None:
+ self._ad = ad
+ self._provider_status_callback = None
+
+ def load_snippet(self) -> None:
+ """Starts the provider simulator snippet and connects.
+
+ Raises:
+ SnippetError: Illegal load operations are attempted.
+ """
+ self._ad.load_snippet(
+ name='fp', package=FP_PROVIDER_SIMULATOR_SNIPPETS_PACKAGE)
+
+ def start_provider_simulator(self, model_id: str,
+ anti_spoofing_key: str) -> None:
+ """Starts the Fast Pair provider simulator.
+
+ Args:
+ model_id: A 3-byte hex string for seeker side to recognize the device (ex:
+ 0x00000C).
+ anti_spoofing_key: A public key for registered headsets.
+ """
+ self._provider_status_callback = self._ad.fp.startProviderSimulator(
+ model_id, anti_spoofing_key)
+
+ def stop_provider_simulator(self) -> None:
+ """Stops the Fast Pair provider simulator."""
+ self._ad.fp.stopProviderSimulator()
+
+ @retry.retry(tries=3)
+ def get_ble_mac_address(self) -> str:
+ """Gets Bluetooth low energy mac address of the provider simulator.
+
+ The BLE mac address will be set by the AdvertisingSet.getOwnAddress()
+ callback. This is the callback flow in the custom Android build. It takes
+ a while after advertising started so we use retry here to wait it.
+
+ Returns:
+ The BLE mac address of the Fast Pair provider simulator.
+ """
+ return self._ad.fp.getBluetoothLeAddress()
+
+ def wait_for_discoverable_mode(self, timeout_seconds: int) -> None:
+ """Waits onScanModeChange event to ensure provider is discoverable.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ """
+
+ def _on_scan_mode_change_event_received(
+ scan_mode_change_event: SnippetEvent, elapsed_time: int) -> bool:
+ scan_mode = scan_mode_change_event.data['mode']
+ self._ad.log.info(
+ 'Provider simulator changed the scan mode to %s in %d seconds.',
+ scan_mode, elapsed_time)
+ return scan_mode == DISCOVERABLE_MODE
+
+ def _on_scan_mode_change_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from provider side '
+ 'after %d seconds...', ON_SCAN_MODE_CHANGE_EVENT, elapsed_time)
+
+ def _on_scan_mode_change_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_SCAN_MODE_CHANGE_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._provider_status_callback,
+ event_name=ON_SCAN_MODE_CHANGE_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_scan_mode_change_event_received,
+ on_waiting=_on_scan_mode_change_event_waiting,
+ on_missed=_on_scan_mode_change_event_missed)
+
+ def wait_for_advertising_start(self, timeout_seconds: int) -> None:
+ """Waits onAdvertisingChange event to ensure provider is advertising.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ """
+
+ def _on_advertising_mode_change_event_received(
+ scan_mode_change_event: SnippetEvent, elapsed_time: int) -> bool:
+ advertising_mode = scan_mode_change_event.data['isAdvertising']
+ self._ad.log.info(
+ 'Provider simulator changed the advertising mode to %s in %d seconds.',
+ advertising_mode, elapsed_time)
+ return advertising_mode
+
+ def _on_advertising_mode_change_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from provider side '
+ 'after %d seconds...', ON_ADVERTISING_CHANGE_EVENT, elapsed_time)
+
+ def _on_advertising_mode_change_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_ADVERTISING_CHANGE_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._provider_status_callback,
+ event_name=ON_ADVERTISING_CHANGE_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_advertising_mode_change_event_received,
+ on_waiting=_on_advertising_mode_change_event_waiting,
+ on_missed=_on_advertising_mode_change_event_missed)
diff --git a/nearby/tests/multidevices/host/fast_pair_seeker.py b/nearby/tests/multidevices/host/fast_pair_seeker.py
new file mode 100644
index 0000000..df02f51
--- /dev/null
+++ b/nearby/tests/multidevices/host/fast_pair_seeker.py
@@ -0,0 +1,91 @@
+"""Fast Pair seeker role."""
+
+from mobly import asserts
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import snippet_event
+
+import event_helper
+
+# The package name of the Nearby Mainline Fast Pair seeker Mobly snippet.
+FP_SEEKER_SNIPPETS_PACKAGE = 'android.nearby.multidevices'
+
+# Events reported from the seeker snippet.
+ON_PROVIDER_FOUND_EVENT = 'onDiscovered'
+
+# Abbreviations for common use type.
+AndroidDevice = android_device.AndroidDevice
+SnippetEvent = snippet_event.SnippetEvent
+wait_for_event = event_helper.wait_callback_event
+
+
+class FastPairSeeker:
+ """A proxy for seeker snippet on the device."""
+
+ def __init__(self, ad: AndroidDevice) -> None:
+ self._ad = ad
+ self._scan_result_callback = None
+
+ def load_snippet(self) -> None:
+ """Starts the seeker snippet and connects.
+
+ Raises:
+ SnippetError: Illegal load operations are attempted.
+ """
+ self._ad.load_snippet(name='fp', package=FP_SEEKER_SNIPPETS_PACKAGE)
+
+ def start_scan(self) -> None:
+ """Starts scanning to find Fast Pair provider devices."""
+ self._scan_result_callback = self._ad.fp.startScan()
+
+ def stop_scan(self) -> None:
+ """Stops the Fast Pair seeker scanning."""
+ self._ad.fp.stopScan()
+
+ def start_pair(self, model_id: str, address: str) -> None:
+ """Starts the Fast Pair seeker pairing.
+
+ Args:
+ model_id: A 3-byte hex string for seeker side to recognize the provider
+ device (ex: 0x00000C).
+ address: The BLE mac address of the Fast Pair provider.
+ """
+ self._ad.log.info('Before calling startPairing')
+ self._ad.fp.startPairing(model_id, address)
+ self._ad.log.info('After calling startPairing')
+
+ def wait_and_assert_provider_found(self, timeout_seconds: int,
+ expected_model_id: str,
+ expected_ble_mac_address: str) -> None:
+ """Waits and asserts any onDiscovered event from the seeker.
+
+ Args:
+ timeout_seconds: The number of seconds to wait before giving up.
+ expected_model_id: The expected model ID of the remote Fast Pair provider
+ device.
+ expected_ble_mac_address: The expected BLE MAC address of the remote Fast
+ Pair provider device.
+ """
+
+ def _on_provider_found_event_received(provider_found_event: SnippetEvent,
+ elapsed_time: int) -> bool:
+ nearby_device_str = provider_found_event.data['device']
+ self._ad.log.info('Seeker discovered first provider(%s) in %d seconds.',
+ nearby_device_str, elapsed_time)
+ return expected_ble_mac_address in nearby_device_str
+
+ def _on_provider_found_event_waiting(elapsed_time: int) -> None:
+ self._ad.log.info(
+ 'Still waiting "%s" event callback from seeker side '
+ 'after %d seconds...', ON_PROVIDER_FOUND_EVENT, elapsed_time)
+
+ def _on_provider_found_event_missed() -> None:
+ asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+ f'the specific "{ON_PROVIDER_FOUND_EVENT}" event.')
+
+ wait_for_event(
+ callback_event_handler=self._scan_result_callback,
+ event_name=ON_PROVIDER_FOUND_EVENT,
+ timeout_seconds=timeout_seconds,
+ on_received=_on_provider_found_event_received,
+ on_waiting=_on_provider_found_event_waiting,
+ on_missed=_on_provider_found_event_missed)
diff --git a/nearby/tests/multidevices/host/seeker_discover_provider_test.py b/nearby/tests/multidevices/host/seeker_discover_provider_test.py
new file mode 100644
index 0000000..f875250
--- /dev/null
+++ b/nearby/tests/multidevices/host/seeker_discover_provider_test.py
@@ -0,0 +1,76 @@
+# Lint as: python3
+"""CTS-V Nearby Mainline Fast Pair end-to-end test case: seeker can discover the provider."""
+
+import logging
+import sys
+
+from mobly import asserts
+from mobly import base_test
+from mobly import test_runner
+from mobly.controllers import android_device
+
+import fast_pair_provider_simulator
+import fast_pair_seeker
+
+# Default model ID to simulate on provider side.
+DEFAULT_MODEL_ID = '00000C'
+# Default public key to simulate as registered headsets.
+DEFAULT_ANTI_SPOOFING_KEY = 'Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE='
+# Default time in seconds for events waiting.
+DEFAULT_TIMEOUT_SEC = 60
+
+# Abbreviations for common use type.
+FastPairProviderSimulator = fast_pair_provider_simulator.FastPairProviderSimulator
+FastPairSeeker = fast_pair_seeker.FastPairSeeker
+
+
+class SeekerDiscoverProviderTest(base_test.BaseTestClass):
+ """Fast Pair seeker discover provider test."""
+
+ _provider: FastPairProviderSimulator
+ _seeker: FastPairSeeker
+
+ def setup_class(self) -> None:
+ super().setup_class()
+ self.duts = self.register_controller(android_device)
+
+ # Assume the 1st phone is provider, the 2nd is seeker.
+ provider_ad, seeker_ad = self.duts
+ provider_ad.debug_tag = 'FastPairProviderSimulator'
+ seeker_ad.debug_tag = 'MainlineFastPairSeeker'
+ self._provider = FastPairProviderSimulator(provider_ad)
+ self._seeker = FastPairSeeker(seeker_ad)
+ self._provider.load_snippet()
+ self._seeker.load_snippet()
+
+ def setup_test(self) -> None:
+ super().setup_test()
+ self._provider.start_provider_simulator(DEFAULT_MODEL_ID,
+ DEFAULT_ANTI_SPOOFING_KEY)
+ self._provider.wait_for_discoverable_mode(DEFAULT_TIMEOUT_SEC)
+ self._provider.wait_for_advertising_start(DEFAULT_TIMEOUT_SEC)
+ self._seeker.start_scan()
+
+ def teardown_test(self) -> None:
+ super().teardown_test()
+ self._seeker.stop_scan()
+ self._provider.stop_provider_simulator()
+ # Create per-test excepts of logcat.
+ for dut in self.duts:
+ dut.services.create_output_excerpts_all(self.current_test_info)
+
+ def test_seeker_start_scanning_find_provider(self) -> None:
+ provider_ble_mac_address = self._provider.get_ble_mac_address()
+ self._seeker.wait_and_assert_provider_found(
+ timeout_seconds=DEFAULT_TIMEOUT_SEC,
+ expected_model_id=DEFAULT_MODEL_ID,
+ expected_ble_mac_address=provider_ble_mac_address)
+
+
+if __name__ == '__main__':
+ # Take test args
+ index = sys.argv.index('--')
+ sys.argv = sys.argv[:1] + sys.argv[index + 1:]
+
+ logging.basicConfig(filename="/tmp/seeker_scan_provider_test_log.txt", level=logging.INFO)
+ test_runner.main()