summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXianyuan Jia <xianyuanjia@google.com>2024-04-02 16:41:38 -0700
committerXianyuan Jia <xianyuanjia@google.com>2024-04-03 11:22:11 -0700
commitdc701bc885e2714a0b2750357d6f67303085b125 (patch)
tree20730adbb9d8d9edaef9c66035716a87b52b29c1
parent83cc696e910cbc4fea12888c37d6a1ba2d85ed6f (diff)
downloadplatform_testing-dc701bc885e2714a0b2750357d6f67303085b125.tar.gz
Initial commit for BetoCQ v2
TODO: - hardware capabilities file - atest targets per test class - script to generate executable test zip - openwrt support Bug: 322046897 Test: atest; python3 with executable zip Change-Id: I120c348b867f7fbf8adb286d2843500401368173
-rw-r--r--tests/bettertogether/betocq/Android.bp228
-rw-r--r--tests/bettertogether/betocq/CHANGELOG.md29
-rw-r--r--tests/bettertogether/betocq/__init__.py13
-rw-r--r--tests/bettertogether/betocq/android_wifi_utils.py203
-rw-r--r--tests/bettertogether/betocq/base_betocq_suite.py83
-rw-r--r--tests/bettertogether/betocq/batch_all_performance_test_suite.py177
-rw-r--r--tests/bettertogether/betocq/batch_performance_testing_with_2g_ap_test_suite.py103
-rw-r--r--tests/bettertogether/betocq/batch_performance_testing_with_5g_ap_test_suite.py74
-rw-r--r--tests/bettertogether/betocq/batch_performance_testing_with_dfs_5g_ap_test_suite.py78
-rw-r--r--tests/bettertogether/betocq/cuj_tests/bt_2g_wifi_coex_test.py101
-rw-r--r--tests/bettertogether/betocq/cuj_tests/local_dev_cuj_testbed.yml77
-rw-r--r--tests/bettertogether/betocq/cuj_tests/mcc_5g_all_wifi_non_dbs_2g_sta_test.py112
-rw-r--r--tests/bettertogether/betocq/cuj_tests/scc_2g_all_wifi_sta_test.py113
-rw-r--r--tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_dbs_2g_sta_test.py112
-rw-r--r--tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_sta_test.py106
-rw-r--r--tests/bettertogether/betocq/d2d_performance_test_base.py705
-rw-r--r--tests/bettertogether/betocq/directed_tests/ble_performance_test.py96
-rw-r--r--tests/bettertogether/betocq/directed_tests/bt_performance_test.py95
-rw-r--r--tests/bettertogether/betocq/directed_tests/local_dev_directed_testbed.yml77
-rw-r--r--tests/bettertogether/betocq/directed_tests/mcc_2g_wfd_indoor_5g_sta_test.py111
-rw-r--r--tests/bettertogether/betocq/directed_tests/mcc_5g_hotspot_dfs_5g_sta_test.py106
-rw-r--r--tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_dfs_5g_sta_test.py101
-rw-r--r--tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_non_dbs_2g_sta_test.py100
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_2g_wfd_sta_test.py100
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_2g_wlan_sta_test.py103
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_5g_wfd_dbs_2g_sta_test.py99
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_5g_wfd_sta_test.py92
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_5g_wlan_sta_test.py91
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_dfs_5g_hotspot_sta_test.py103
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_dfs_5g_wfd_sta_test.py103
-rw-r--r--tests/bettertogether/betocq/directed_tests/scc_indoor_5g_wfd_sta_test.py103
-rw-r--r--tests/bettertogether/betocq/function_tests/beto_cq_function_group_test.py139
-rw-r--r--tests/bettertogether/betocq/function_tests/bt_ble_function_test_actor.py88
-rw-r--r--tests/bettertogether/betocq/function_tests/bt_multiplex_function_test_actor.py133
-rw-r--r--tests/bettertogether/betocq/function_tests/fixed_wifi_medium_function_test_actor.py150
-rw-r--r--tests/bettertogether/betocq/function_tests/function_test_actor_base.py74
-rw-r--r--tests/bettertogether/betocq/function_tests/local_dev_function_group_testbed.yml26
-rw-r--r--tests/bettertogether/betocq/gms_auto_updates_util.py176
-rw-r--r--tests/bettertogether/betocq/local_dev_testbed.yml102
-rw-r--r--tests/bettertogether/betocq/nc_base_test.py271
-rw-r--r--tests/bettertogether/betocq/nc_constants.py366
-rw-r--r--tests/bettertogether/betocq/nearby_connection_wrapper.py417
-rw-r--r--tests/bettertogether/betocq/setup_utils.py390
-rw-r--r--tests/bettertogether/betocq/version.py21
44 files changed, 6147 insertions, 0 deletions
diff --git a/tests/bettertogether/betocq/Android.bp b/tests/bettertogether/betocq/Android.bp
new file mode 100644
index 000000000..11a9afa5f
--- /dev/null
+++ b/tests/bettertogether/betocq/Android.bp
@@ -0,0 +1,228 @@
+// Copyright (C) 2024 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"],
+ default_team: "trendy_team_fwk_uwb",
+}
+
+python_defaults {
+ name: "betocq_lib_defaults",
+ pkg_path: "betocq",
+}
+
+python_defaults {
+ name: "betocq_test_defaults",
+ version: {
+ py3: {
+ embedded_launcher: false,
+ },
+ },
+}
+
+// Libraries
+
+python_library_host {
+ name: "betocq_lib",
+ defaults: ["betocq_lib_defaults"],
+ srcs: [
+ "android_wifi_utils.py",
+ "gms_auto_updates_util.py",
+ "nc_constants.py",
+ "nearby_connection_wrapper.py",
+ "setup_utils.py",
+ "version.py",
+ ],
+ libs: [
+ "mobly",
+ ],
+}
+
+python_library_host {
+ name: "base_betocq_suite",
+ defaults: ["betocq_lib_defaults"],
+ srcs: [
+ "base_betocq_suite.py",
+ ],
+ libs: [
+ "mobly",
+ "pyyaml",
+ ],
+}
+
+python_library_host {
+ name: "d2d_performance_test_base",
+ defaults: ["betocq_lib_defaults"],
+ srcs: [
+ "d2d_performance_test_base.py",
+ ],
+ libs: [
+ "betocq_lib",
+ "mobly",
+ "betocq_nc_base_test",
+ ],
+}
+
+python_library_host {
+ name: "betocq_nc_base_test",
+ defaults: ["betocq_lib_defaults"],
+ srcs: [
+ "nc_base_test.py",
+ ],
+ libs: [
+ "betocq_lib",
+ "mobly",
+ "pyyaml",
+ ],
+}
+
+python_library_host {
+ name: "betocq_cuj_tests",
+ defaults: ["betocq_lib_defaults"],
+ srcs: ["cuj_tests/*.py"],
+ libs: [
+ "betocq_lib",
+ "d2d_performance_test_base",
+ "mobly",
+ ],
+}
+
+python_library_host {
+ name: "betocq_directed_tests",
+ defaults: ["betocq_lib_defaults"],
+ srcs: ["directed_tests/*.py"],
+ libs: [
+ "betocq_lib",
+ "d2d_performance_test_base",
+ "mobly",
+ ],
+}
+
+python_library_host {
+ name: "betocq_function_tests",
+ defaults: ["betocq_lib_defaults"],
+ srcs: ["function_tests/*.py"],
+ libs: [
+ "betocq_lib",
+ "mobly",
+ ],
+}
+
+// TODO: Add modules for individual test classes.
+
+// Test suites
+
+python_test_host {
+ name: "batch_all_performance_test_suite",
+ main: "batch_all_performance_test_suite.py",
+ srcs: ["batch_all_performance_test_suite.py"],
+ libs: [
+ "base_betocq_suite",
+ "betocq_cuj_tests",
+ "betocq_directed_tests",
+ "betocq_function_tests",
+ "betocq_lib",
+ "mobly",
+ ],
+ data: [
+ "local_dev_testbed.yml",
+ // package the snippes for atest
+ ":nearby_snippet",
+ ":nearby_snippet_2",
+ ],
+ test_suites: [],
+ test_options: {
+ unit_test: false, // as Mobly tests require device(s)
+ // This tag is used to enable the ATest Mobly runner
+ tags: ["mobly"],
+ },
+}
+
+python_test_host {
+ name: "batch_performance_testing_with_2g_ap_test_suite",
+ main: "batch_performance_testing_with_2g_ap_test_suite.py",
+ srcs: ["batch_performance_testing_with_2g_ap_test_suite.py"],
+ defaults: ["betocq_test_defaults"],
+ libs: [
+ "base_betocq_suite",
+ "betocq_cuj_tests",
+ "betocq_directed_tests",
+ "betocq_function_tests",
+ "betocq_lib",
+ "mobly",
+ ],
+ data: [
+ "local_dev_testbed.yml",
+ // package the snippes for atest
+ ":nearby_snippet",
+ ":nearby_snippet_2",
+ ],
+ test_suites: [],
+ test_options: {
+ unit_test: false, // as Mobly tests require device(s)
+ // This tag is used to enable the ATest Mobly runner
+ tags: ["mobly"],
+ },
+}
+
+python_test_host {
+ name: "batch_performance_testing_with_5g_ap_test_suite",
+ main: "batch_performance_testing_with_5g_ap_test_suite.py",
+ srcs: ["batch_performance_testing_with_5g_ap_test_suite.py"],
+ defaults: ["betocq_test_defaults"],
+ libs: [
+ "base_betocq_suite",
+ "betocq_directed_tests",
+ "betocq_lib",
+ "mobly",
+ ],
+ data: [
+ "local_dev_testbed.yml",
+ // package the snippes for atest
+ ":nearby_snippet",
+ ":nearby_snippet_2",
+ ],
+ test_suites: [],
+ test_options: {
+ unit_test: false, // as Mobly tests require device(s)
+ // This tag is used to enable the ATest Mobly runner
+ tags: ["mobly"],
+ },
+}
+
+python_test_host {
+ name: "batch_performance_testing_with_dfs_5g_ap_test_suite",
+ main: "batch_performance_testing_with_dfs_5g_ap_test_suite.py",
+ srcs: ["batch_performance_testing_with_dfs_5g_ap_test_suite.py"],
+ defaults: ["betocq_test_defaults"],
+ libs: [
+ "base_betocq_suite",
+ "betocq_cuj_tests",
+ "betocq_directed_tests",
+ "betocq_lib",
+ "mobly",
+ ],
+ data: [
+ "local_dev_testbed.yml",
+ // package the snippes for atest
+ ":nearby_snippet",
+ ":nearby_snippet_2",
+ ],
+ test_suites: [],
+ test_options: {
+ unit_test: false, // as Mobly tests require device(s)
+ // This tag is used to enable the ATest Mobly runner
+ tags: ["mobly"],
+ },
+}
diff --git a/tests/bettertogether/betocq/CHANGELOG.md b/tests/bettertogether/betocq/CHANGELOG.md
new file mode 100644
index 000000000..632c69acc
--- /dev/null
+++ b/tests/bettertogether/betocq/CHANGELOG.md
@@ -0,0 +1,29 @@
+# BetoCQ test suite release history
+
+## 2.0
+
+### New
+* BetoCQ test suite: more accurate connectivity quality test suite with better
+coverage.
+ * CUJ tests: Connectivity performance evaluation for the specific CUJs, such
+ as Quickstart, Quickshare, etc.
+ * Directed tests: Specific performance tests for mediums used by D2D
+ connection.
+ * Function tests: Tests for the basic functions used by D2D connection.
+
+## 1.6
+
+### New
+* `nearby_share_stress_test.py` for testing Nearby Share using Wifi only.
+
+### Fixes
+* Change discovery medium to BLE only.
+* Increase 1G file transfer timeout to 400s.
+* Disable GMS auto-updates for the duration of the test.
+
+## 1.5
+
+### New
+* `esim_transfer_stress_test.py` for testing eSIM transfer using Bluetooth only.
+* `quick_start_stress_test.py` for testing the Quickstart flow using both
+ Bluetooth and Wifi. \ No newline at end of file
diff --git a/tests/bettertogether/betocq/__init__.py b/tests/bettertogether/betocq/__init__.py
new file mode 100644
index 000000000..f915584ae
--- /dev/null
+++ b/tests/bettertogether/betocq/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 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.
diff --git a/tests/bettertogether/betocq/android_wifi_utils.py b/tests/bettertogether/betocq/android_wifi_utils.py
new file mode 100644
index 000000000..1fe84be6c
--- /dev/null
+++ b/tests/bettertogether/betocq/android_wifi_utils.py
@@ -0,0 +1,203 @@
+# Copyright (C) 2024 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.
+
+"""Utils for Android Wi-Fi operations."""
+
+import dataclasses
+import datetime
+import enum
+import re
+import time
+
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import adb
+
+_DELAY_AFTER_CHANGE_WIFI_STATUS = datetime.timedelta(seconds=5)
+_WAIT_FOR_CONNECTION = datetime.timedelta(seconds=30)
+
+_SAVED_WIFI_LIST_PATTERN = re.compile(
+ r'(?P<id>\d+)\s+(?P<ssid>.*)\s+(?P<security>.*)'
+)
+_SSID_PATTERN = re.compile(rb'Wifi is connected to "(?P<ssid>.*?)"')
+
+
+@dataclasses.dataclass(frozen=True)
+class SavedWifiInfo:
+ """Information about a saved Wi-Fi network."""
+
+ id: str
+ ssid: str
+ security: str
+
+
+@enum.unique
+class WiFiSecurity(enum.StrEnum):
+ """Security type of the Wi-Fi network."""
+
+ OPEN = 'open'
+ OWE = 'owe'
+ WPA2 = 'wpa2'
+ WPA3 = 'wpa3'
+ WEP = 'wep'
+
+
+class AndroidWiFiError(Exception):
+ """Error when failed to operate Wi-Fi on Android device."""
+
+ def __init__(self, ad: android_device.AndroidDevice, message: str) -> None:
+ self._ad = ad
+ self._message = message
+
+ def __str__(self) -> str:
+ return self._message if self._ad is None else f'{self._ad} {self._message}'
+
+
+def connect_to_wifi(
+ ad: android_device.AndroidDevice,
+ ssid: str,
+ passphrase: str | None = None,
+ security: WiFiSecurity | None = None,
+) -> None:
+ """Connects to a W-Fi network and adds to saved networks list."""
+ enable_wifi(ad)
+ if get_current_wifi(ad) == ssid:
+ ad.log.info(f'Wi-Fi was already connected to {repr(ssid)}')
+ return
+ cmd = ['cmd', 'wifi', 'connect-network', f'"{ssid}"']
+ if passphrase is None:
+ cmd.append(f'"{security or WiFiSecurity.OPEN}"')
+ else:
+ cmd.extend([f'"{security or WiFiSecurity.WPA2}"', f'"{passphrase}"'])
+ ad.adb.shell(cmd)
+ if not _wait_for_data_connected(ad) or get_current_wifi(ad) != ssid:
+ raise AndroidWiFiError(ad, f'Fail to connect to Wi-Fi {repr(ssid)}')
+ ad.log.info(f'Wi-Fi connected to {repr(ssid)}')
+
+
+def disable_wifi(ad: android_device.AndroidDevice) -> None:
+ """Disables Wi-Fi."""
+ if not is_wifi_enabled(ad):
+ ad.log.info('Wi-Fi was already disabled.')
+ return
+ ad.log.info('Disabling Wi-Fi...')
+ ad.adb.shell(['cmd', 'wifi', 'set-wifi-enabled', 'disabled'])
+ start_time = time.monotonic()
+ timeout = start_time + _DELAY_AFTER_CHANGE_WIFI_STATUS.total_seconds()
+ while time.monotonic() < timeout:
+ if not is_wifi_enabled(ad):
+ ad.log.info('Wi-Fi is disabled.')
+ return
+ raise AndroidWiFiError(
+ ad,
+ 'Fail to disable Wi-Fi after waiting for'
+ f' {_DELAY_AFTER_CHANGE_WIFI_STATUS.total_seconds()} seconds',
+ )
+
+
+def enable_wifi(ad: android_device.AndroidDevice) -> None:
+ """Enables Wi-Fi."""
+ if is_wifi_enabled(ad):
+ ad.log.info('Wi-Fi was already enabled.')
+ return
+ ad.log.info('Enabling Wi-Fi...')
+ ad.adb.shell(['cmd', 'wifi', 'set-wifi-enabled', 'enabled'])
+ start_time = time.monotonic()
+ timeout = start_time + _DELAY_AFTER_CHANGE_WIFI_STATUS.total_seconds()
+ while time.monotonic() < timeout:
+ if is_wifi_enabled(ad):
+ ad.log.info('Wi-Fi is enabled.')
+ return
+ raise AndroidWiFiError(
+ ad,
+ 'Fail to enable Wi-Fi after waiting for'
+ f' {_DELAY_AFTER_CHANGE_WIFI_STATUS.total_seconds()} seconds',
+ )
+
+
+def forget_all_wifi(ad: android_device.AndroidDevice) -> None:
+ """Forgets all Wi-Fi from saved networks list."""
+ for saved_wifi in list_saved_wifi(ad):
+ ad.adb.shell(['cmd', 'wifi', 'forget-network', saved_wifi.id])
+ saved_wifis = list_saved_wifi(ad)
+ if saved_wifis:
+ raise AndroidWiFiError(
+ ad,
+ 'Fail to forget all Wi-Fi networks, remaining in the list:'
+ f' {saved_wifis}',
+ )
+
+
+def forget_wifi(ad: android_device.AndroidDevice, ssid: str) -> None:
+ """Forgets Wi-Fi from saved networks list."""
+ saved_wifis = list_saved_wifi(ad)
+ for saved_wifi in saved_wifis:
+ if saved_wifi.ssid == ssid:
+ stdout = ad.adb.shell(['cmd', 'wifi', 'forget-network', saved_wifi.id])
+ if stdout == b'Forget successful\n':
+ ad.log.info(f'Wi-Fi {repr(ssid)} was forgotten from saved networks.')
+ return
+ raise AndroidWiFiError(ad, f'Fail to forget Wi-Fi {repr(ssid)}')
+ ad.log.info(f'Nothing was deleted since Wi-Fi {repr(ssid)} was not saved')
+ return
+
+
+def get_current_wifi(ad: android_device.AndroidDevice) -> str:
+ """Returns current Wi-Fi network."""
+ if match := _SSID_PATTERN.search(ad.adb.shell(['cmd', 'wifi', 'status'])):
+ return match.group('ssid').decode()
+ return ''
+
+
+def is_wifi_enabled(ad: android_device.AndroidDevice) -> bool:
+ """Returns True if Wi-Fi is enabled, False otherwise."""
+ return ad.adb.shell(['cmd', 'wifi', 'status']).startswith(b'Wifi is enabled')
+
+
+def list_saved_wifi(ad: android_device.AndroidDevice) -> list[SavedWifiInfo]:
+ """Returns list of saved Wi-Fi networks."""
+ saved_wifis = []
+ stdout = ad.adb.shell(['cmd', 'wifi', 'list-networks']).decode()
+ if stdout == 'No networks\n':
+ return saved_wifis
+ for line in stdout.splitlines()[1:]:
+ if match := _SAVED_WIFI_LIST_PATTERN.search(line):
+ saved_wifis.append(
+ SavedWifiInfo(
+ id=match.group('id'),
+ ssid=match.group('ssid').strip(),
+ security=match.group('security'),
+ )
+ )
+ return saved_wifis
+
+
+def _is_data_connected(ad: android_device.AndroidDevice) -> bool:
+ """Returns True if data is connected, False otherwise."""
+ try:
+ return b'5 received' in ad.adb.shell(['ping', '-c', '5', '8.8.8.8'])
+ except adb.AdbError:
+ return False
+
+
+def _wait_for_data_connected(
+ ad: android_device.AndroidDevice,
+ timeout: datetime.timedelta = _WAIT_FOR_CONNECTION,
+) -> bool:
+ """Returns True if data is connected before timeout, False otherwise."""
+ start_time = time.monotonic()
+ timeout = start_time + timeout.total_seconds()
+ while time.monotonic() < timeout:
+ if _is_data_connected(ad):
+ return True
+ return False
diff --git a/tests/bettertogether/betocq/base_betocq_suite.py b/tests/bettertogether/betocq/base_betocq_suite.py
new file mode 100644
index 000000000..1bc29668f
--- /dev/null
+++ b/tests/bettertogether/betocq/base_betocq_suite.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2024 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.
+
+"""Base class for BetoCQ test suites."""
+
+import logging
+import os
+
+from mobly import base_suite
+from mobly import records
+import yaml
+
+
+class BaseBetocqSuite(base_suite.BaseSuite):
+ """Base class for BetoCQ test suites.
+
+ Contains methods for aggregating and exporting suite data.
+ """
+
+ def __init__(self, runner, config):
+ super().__init__(runner, config)
+ self._summary_path = None
+ self._summary_writer = None
+
+ @property
+ def summary_path(self):
+ """Returns the path to the summary file.
+
+ NOTE: This path is only correctly resolved if called within teardown_suite.
+ """
+ if self._summary_path is None:
+ # pylint: disable-next=protected-access
+ self._summary_path = self._runner._test_run_metadata.summary_file_path
+ return self._summary_path
+
+ def _retrieve_user_data_from_summary(self):
+ """Retrieves all user_data entries from the currently streamed summary.
+
+ Use this method to aggregate data written by record_data in test classes.
+
+ NOTE: This method can only be called within teardown_suite.
+
+ Returns:
+ A list of dictionaries, each corresponding to a USER_DATA entry.
+ """
+ if not os.path.isfile(self.summary_path):
+ logging.error(
+ 'Cannot retrieve user data for the suite. '
+ 'The summary file does not exist: %s',
+ self.summary_path,
+ )
+ return []
+
+ with open(self.summary_path, 'r') as f:
+ return [
+ entry
+ for entry in yaml.safe_load_all(f)
+ if entry['Type'] == records.TestSummaryEntryType.USER_DATA.value
+ ]
+
+ def _record_suite_properties(self, properties):
+ """Record suite properties to the test summary file.
+
+ NOTE: This method can only be called within teardown_suite.
+
+ Args:
+ properties: dict, the properties to add to the summary
+ """
+ if self._summary_writer is None:
+ self._summary_writer = records.TestSummaryWriter(self.summary_path)
+ content = {'properties': properties}
+ self._summary_writer.dump(content, records.TestSummaryEntryType.USER_DATA)
diff --git a/tests/bettertogether/betocq/batch_all_performance_test_suite.py b/tests/bettertogether/betocq/batch_all_performance_test_suite.py
new file mode 100644
index 000000000..844f2034a
--- /dev/null
+++ b/tests/bettertogether/betocq/batch_all_performance_test_suite.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2024 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.
+
+"""This test suite batches all tests to run in sequence.
+
+This requires 3 APs to be ready and configured in testbed.
+2G AP (wifi_2g_ssid): channel 6 (2437)
+5G AP (wifi_5g_ssid): channel 36 (5180)
+DFS 5G AP(wifi_dfs_5g_ssid): channel 52 (5260)
+"""
+
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import suite_runner
+
+from betocq import base_betocq_suite
+from betocq import nc_constants
+from betocq.cuj_tests import mcc_5g_all_wifi_non_dbs_2g_sta_test
+from betocq.cuj_tests import scc_2g_all_wifi_sta_test
+from betocq.cuj_tests import scc_5g_all_wifi_dbs_2g_sta_test
+from betocq.cuj_tests import scc_5g_all_wifi_sta_test
+from betocq.directed_tests import bt_performance_test
+from betocq.directed_tests import mcc_2g_wfd_indoor_5g_sta_test
+from betocq.directed_tests import mcc_5g_hotspot_dfs_5g_sta_test
+from betocq.directed_tests import mcc_5g_wfd_dfs_5g_sta_test
+from betocq.directed_tests import mcc_5g_wfd_non_dbs_2g_sta_test
+from betocq.directed_tests import scc_2g_wfd_sta_test
+from betocq.directed_tests import scc_2g_wlan_sta_test
+from betocq.directed_tests import scc_5g_wfd_dbs_2g_sta_test
+from betocq.directed_tests import scc_5g_wfd_sta_test
+from betocq.directed_tests import scc_5g_wlan_sta_test
+from betocq.directed_tests import scc_dfs_5g_hotspot_sta_test
+from betocq.directed_tests import scc_dfs_5g_wfd_sta_test
+from betocq.directed_tests import scc_indoor_5g_wfd_sta_test
+from betocq.function_tests import beto_cq_function_group_test
+
+
+class BetoCqPerformanceTestSuite(base_betocq_suite.BaseBetocqSuite):
+ """Add all BetoCQ tests to run in sequence."""
+
+ def setup_suite(self, config):
+ """Add all BetoCQ tests to the suite."""
+ test_parameters = nc_constants.TestParameters.from_user_params(
+ config.user_params
+ )
+
+ if test_parameters.target_cuj_name is nc_constants.TARGET_CUJ_ESIM:
+ self.add_test_class(bt_performance_test.BtPerformanceTest)
+ return
+
+ # add function tests if required
+ if (
+ test_parameters.run_function_tests_with_performance_tests
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ self.add_test_class(
+ beto_cq_function_group_test.BetoCqFunctionGroupTest
+ )
+
+ # add bt and ble test
+ self.add_test_class(bt_performance_test.BtPerformanceTest)
+
+ # TODO(kaishi): enable BLE test when it is ready
+
+ # add directed/cuj tests which requires 2G wlan AP - channel 6
+ if (
+ test_parameters.wifi_2g_ssid
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ config = self._config.copy()
+ config.user_params['wifi_channel'] = 6
+
+ self.add_test_class(
+ clazz=mcc_5g_wfd_non_dbs_2g_sta_test.Mcc5gWfdNonDbs2gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_2g_wfd_sta_test.Scc2gWfdStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_2g_wlan_sta_test.Scc2gWlanStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_5g_wfd_dbs_2g_sta_test.Scc5gWfdDbs2gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=mcc_5g_all_wifi_non_dbs_2g_sta_test.Mcc5gAllWifiNonDbs2gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_2g_all_wifi_sta_test.Scc2gAllWifiStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_5g_all_wifi_dbs_2g_sta_test.Scc5gAllWifiDbs2gStaTest,
+ config=config,
+ )
+
+ # add directed tests which requires 5G wlan AP - channel 36
+ if (
+ test_parameters.wifi_5g_ssid
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ config = self._config.copy()
+ config.user_params['wifi_channel'] = 36
+
+ self.add_test_class(
+ clazz=mcc_2g_wfd_indoor_5g_sta_test.Mcc2gWfdIndoor5gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_5g_wfd_sta_test.Scc5gWfdStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_5g_wlan_sta_test.Scc5gWifiLanStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_indoor_5g_wfd_sta_test.SccIndoor5gWfdStaTest,
+ config=config,
+ )
+
+ # add directed/cuj tests which requires DFS 5G wlan AP - channel 52
+ if (
+ test_parameters.wifi_dfs_5g_ssid
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ config = self._config.copy()
+ config.user_params['wifi_channel'] = 52
+
+ self.add_test_class(
+ clazz=mcc_5g_hotspot_dfs_5g_sta_test.Mcc5gHotspotDfs5gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=mcc_5g_wfd_dfs_5g_sta_test.Mcc5gWfdDfs5gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_dfs_5g_hotspot_sta_test.SccDfs5gHotspotStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_dfs_5g_wfd_sta_test.SccDfs5gWfdStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ clazz=scc_5g_all_wifi_sta_test.Scc5gAllWifiStaTest,
+ config=config,
+ )
+
+
+if __name__ == '__main__':
+ # Use suite_runner's `main`.
+ suite_runner.run_suite_class()
diff --git a/tests/bettertogether/betocq/batch_performance_testing_with_2g_ap_test_suite.py b/tests/bettertogether/betocq/batch_performance_testing_with_2g_ap_test_suite.py
new file mode 100644
index 000000000..fd0767fbc
--- /dev/null
+++ b/tests/bettertogether/betocq/batch_performance_testing_with_2g_ap_test_suite.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2024 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.
+
+"""This test suite batches all tests to run in sequence.
+
+This requires a 2G AP to be ready and configured in testbed.
+2G AP (wifi_2g_ssid): channel 6 (2437)
+"""
+
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import suite_runner
+
+from betocq import base_betocq_suite
+from betocq import nc_constants
+from betocq.cuj_tests import mcc_5g_all_wifi_non_dbs_2g_sta_test
+from betocq.cuj_tests import scc_2g_all_wifi_sta_test
+from betocq.cuj_tests import scc_5g_all_wifi_dbs_2g_sta_test
+from betocq.directed_tests import ble_performance_test
+from betocq.directed_tests import bt_performance_test
+from betocq.directed_tests import mcc_5g_wfd_non_dbs_2g_sta_test
+from betocq.directed_tests import scc_2g_wfd_sta_test
+from betocq.directed_tests import scc_2g_wlan_sta_test
+from betocq.directed_tests import scc_5g_wfd_dbs_2g_sta_test
+from betocq.function_tests import beto_cq_function_group_test
+
+
+class BetoCq2gApPerformanceTestSuite(base_betocq_suite.BaseBetocqSuite):
+ """Add all BetoCQ tests which requires 2G STA AP or no AP to run in sequence."""
+
+ def setup_suite(self, config):
+ """Add all BetoCQ tests which requires 2G STA AP or no AP to the suite."""
+ test_parameters = nc_constants.TestParameters.from_user_params(
+ config.user_params
+ )
+
+ # add function tests if required
+ if test_parameters.run_function_tests_with_performance_tests:
+ self.add_test_class(
+ beto_cq_function_group_test.BetoCqFunctionGroupTest,
+ config=config,
+ )
+
+ # add bt and ble test
+ self.add_test_class(
+ bt_performance_test.BtPerformanceTest, config=config
+ )
+ # TODO(kaishi): enable BLE test when it is ready
+
+ # add directed/cuj tests which requires 2G wlan AP - channel 6
+ if (
+ test_parameters.wifi_2g_ssid
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ self.add_test_class(
+ mcc_5g_wfd_non_dbs_2g_sta_test.Mcc5gWfdNonDbs2gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_2g_wfd_sta_test.Scc2gWfdStaTest, config=config
+ )
+ self.add_test_class(
+ scc_2g_wlan_sta_test.Scc2gWlanStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_5g_wfd_dbs_2g_sta_test.Scc5gWfdDbs2gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ mcc_5g_all_wifi_non_dbs_2g_sta_test.Mcc5gAllWifiNonDbs2gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_2g_all_wifi_sta_test.Scc2gAllWifiStaTest, config=config
+ )
+ self.add_test_class(
+ scc_5g_all_wifi_dbs_2g_sta_test.Scc5gAllWifiDbs2gStaTest,
+ config=config,
+ )
+
+if __name__ == '__main__':
+ # Use suite_runner's `main`.
+ suite_runner.run_suite_class()
+
diff --git a/tests/bettertogether/betocq/batch_performance_testing_with_5g_ap_test_suite.py b/tests/bettertogether/betocq/batch_performance_testing_with_5g_ap_test_suite.py
new file mode 100644
index 000000000..8b50d82cd
--- /dev/null
+++ b/tests/bettertogether/betocq/batch_performance_testing_with_5g_ap_test_suite.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2024 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.
+
+"""This test suite batches all tests to run in sequence.
+
+This requires a 5G AP to be ready and configured in testbed.
+5G AP (wifi_5g_ssid): channel 36 (5180)
+"""
+
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import suite_runner
+
+from betocq import base_betocq_suite
+from betocq import nc_constants
+from betocq.directed_tests import mcc_2g_wfd_indoor_5g_sta_test
+from betocq.directed_tests import scc_5g_wfd_sta_test
+from betocq.directed_tests import scc_5g_wlan_sta_test
+from betocq.directed_tests import scc_indoor_5g_wfd_sta_test
+
+
+class BetoCq5gApPerformanceTestSuite(base_betocq_suite.BaseBetocqSuite):
+ """Add BetoCQ tests which requires a 5G STA AP to run in sequence."""
+
+ def setup_suite(self, config):
+ """Add BetoCQ tests which requires a 5G STA AP to the suite."""
+ test_parameters = nc_constants.TestParameters.from_user_params(
+ config.user_params
+ )
+
+ # add directed tests which requires 5G wlan AP - channel 36
+ if (
+ test_parameters.wifi_5g_ssid
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ self.add_test_class(
+ mcc_2g_wfd_indoor_5g_sta_test.Mcc2gWfdIndoor5gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_5g_wfd_sta_test.Scc5gWfdStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_5g_wlan_sta_test.Scc5gWifiLanStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_indoor_5g_wfd_sta_test.SccIndoor5gWfdStaTest,
+ config=config,
+ )
+
+if __name__ == '__main__':
+ # Use suite_runner's `main`.
+ suite_runner.run_suite_class()
+
diff --git a/tests/bettertogether/betocq/batch_performance_testing_with_dfs_5g_ap_test_suite.py b/tests/bettertogether/betocq/batch_performance_testing_with_dfs_5g_ap_test_suite.py
new file mode 100644
index 000000000..754183bec
--- /dev/null
+++ b/tests/bettertogether/betocq/batch_performance_testing_with_dfs_5g_ap_test_suite.py
@@ -0,0 +1,78 @@
+# Copyright (C) 2024 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.
+
+"""This test suite batches all tests to run in sequence.
+
+This requires a DFS 5G AP to be ready and configured in testbed.
+DFS 5G AP(wifi_dfs_5g_ssid): channel 52 (5260)
+"""
+
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import suite_runner
+
+from betocq import base_betocq_suite
+from betocq import nc_constants
+
+from betocq.cuj_tests import scc_5g_all_wifi_sta_test
+from betocq.directed_tests import mcc_5g_hotspot_dfs_5g_sta_test
+from betocq.directed_tests import mcc_5g_wfd_dfs_5g_sta_test
+from betocq.directed_tests import scc_dfs_5g_hotspot_sta_test
+from betocq.directed_tests import scc_dfs_5g_wfd_sta_test
+
+
+class BetoCqPerformanceTestSuite(base_betocq_suite.BaseBetocqSuite):
+ """Add all BetoCQ tests to run in sequence."""
+
+ def setup_suite(self, config):
+ """Add all BetoCQ tests to the suite."""
+ test_parameters = nc_constants.TestParameters.from_user_params(
+ config.user_params
+ )
+ # add directed/cuj tests which requires DFS 5G wlan AP - channel 52
+ if (
+ test_parameters.wifi_dfs_5g_ssid
+ or test_parameters.use_auto_controlled_wifi_ap
+ ):
+ self.add_test_class(
+ mcc_5g_hotspot_dfs_5g_sta_test.Mcc5gHotspotDfs5gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ mcc_5g_wfd_dfs_5g_sta_test.Mcc5gWfdDfs5gStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_dfs_5g_hotspot_sta_test.SccDfs5gHotspotStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_dfs_5g_wfd_sta_test.SccDfs5gWfdStaTest,
+ config=config,
+ )
+ self.add_test_class(
+ scc_5g_all_wifi_sta_test.Scc5gAllWifiStaTest,
+ config=config,
+ )
+
+if __name__ == '__main__':
+ # Use suite_runner's `main`.
+ suite_runner.run_suite_class()
diff --git a/tests/bettertogether/betocq/cuj_tests/bt_2g_wifi_coex_test.py b/tests/bettertogether/betocq/cuj_tests/bt_2g_wifi_coex_test.py
new file mode 100644
index 000000000..4370eea23
--- /dev/null
+++ b/tests/bettertogether/betocq/cuj_tests/bt_2g_wifi_coex_test.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the bluetooth and wifi 2G coex.
+
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+_PERFORMANCE_TEST_COUNT = 100
+_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 10
+
+
+class Bt2gWifiCoexTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for BT and 2G wifi coex with a complicated stress test."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ self.test_parameters.requires_bt_multiplex = True
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_bt_2g_wifi_coex, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_bt_2g_wifi_coex(self):
+ """Test the BT and 2G wifi coex with a stress test."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_ALL_WIFI,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_transfer_file_size(self) -> int:
+ # For 2G wifi medium
+ return nc_constants.TRANSFER_FILE_SIZE_20MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related log.'
+ )
+
+ def _get_throughout_benchmark(self) -> int:
+ # no requirement for throughput.
+ return 0
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}.'
+ ' This should never happen, you may ignore this error, this is'
+ ' not required for this case.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return True
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/cuj_tests/local_dev_cuj_testbed.yml b/tests/bettertogether/betocq/cuj_tests/local_dev_cuj_testbed.yml
new file mode 100644
index 000000000..2eeee66bd
--- /dev/null
+++ b/tests/bettertogether/betocq/cuj_tests/local_dev_cuj_testbed.yml
@@ -0,0 +1,77 @@
+x-test-params: &test-params
+ # target_cuj_name: "quick_start"
+ allow_unrooted_device: False
+ wifi_2g_ssid: "NETGEAR62"
+ wifi_2g_password: "widetree588"
+ wifi_5g_ssid: "NETGEAR62-5G-1"
+ wifi_5g_password: "widetree588"
+ wifi_dfs_5g_ssid: "GoogleGuest"
+ wifi_dfs_5g_password: ""
+ # use the AP controlled by programming dynamically
+ use_auto_controlled_wifi_ap: False
+
+x-controllers: &controllers
+ Controllers:
+ AndroidDevice:
+ - serial: "R3CN90YNAR" # "R3CN90YNAR"
+ role: "source_device"
+ # The max number of spatial streams
+ max_num_streams: 2
+ # The max PHY rate at 5G, in Mbps
+ max_phy_rate_5g_mbps: 2402
+ # The max PHY rate at 2G, in Mbps
+ max_phy_rate_2g_mbps: 287
+ # if the device supports 5G Wifi
+ supports_5g: True # True
+ # if the device supports DBS (Dual Band Simultaneous) in STA + WiFi-Direct concurrency mode
+ supports_dbs_sta_wfd: True
+ # The max number of spatial streams in DBS mode.
+ max_num_streams_dbs: 2
+ # if the device supports to start WFD group owner at a STA-associated DFS channel
+ enable_sta_dfs_channel_for_peer_network: False
+ # if the device supports to start WFD group owner at a STA-associated indoor channel
+ enable_sta_indoor_channel_for_peer_network: False
+ # 14 - U, 13 - T, 12 - S
+ android_version: 14
+ - serial: "2B031FDJH0002G" # "ABCDEF123456"
+ role: "target_device"
+ # The max number of spatial streams
+ max_num_streams: 2
+ # The max PHY rate at 5G, in Mbps
+ max_phy_rate_5g_mbps: 2402
+ # The max PHY rate at 2G, in Mbps
+ max_phy_rate_2g_mbps: 287
+ # if the device supports 5G Wifi
+ supports_5g: True # True
+ # if the device supports DBS (Dual Band Simultaneous) in STA + WiFi-Direct concurrency mode
+ supports_dbs_sta_wfd: True
+ # The max number of spatial streams in DBS mode.
+ max_num_streams_dbs: 2
+ # if the device supports to start WFD group owner at a STA-associated DFS channel
+ enable_sta_dfs_channel_for_peer_network: True
+ # if the device supports to start WFD group owner at a STA-associated indoor channel
+ enable_sta_indoor_channel_for_peer_network: True
+ # 14 - U, 13 - T, 12 - S
+ android_version: 14
+
+TestBeds:
+- Name: LocalPerformanceDefaultTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+
+- Name: LocalQuickstartPerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_start"
+ # before the performance test, run the function tests first
+ run_function_tests_with_performance_tests: True
+ # if the function tests is failed, abort the performance test
+ abort_all_tests_on_function_tests_fail: True
+
+- Name: LocalQuicksharePerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_share" \ No newline at end of file
diff --git a/tests/bettertogether/betocq/cuj_tests/mcc_5g_all_wifi_non_dbs_2g_sta_test.py b/tests/bettertogether/betocq/cuj_tests/mcc_5g_all_wifi_non_dbs_2g_sta_test.py
new file mode 100644
index 000000000..f49cb09aa
--- /dev/null
+++ b/tests/bettertogether/betocq/cuj_tests/mcc_5g_all_wifi_non_dbs_2g_sta_test.py
@@ -0,0 +1,112 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi MCC in a general case.
+
+In this case, even though the expected wifi medium is the WFD, but the wifi D2D
+could be any wifi mediums, such as WFD, HOTSPOT, WifiLan; Once the WFD is
+failed, other meidums will be tried. As DBS is not supported by both devices,
+and the STA is coonected with 2G channel, D2D medium is using a 5G channel,
+this cause the MCC case.
+
+The device requirements:
+ support 5G: true
+ support DBS (target device): false
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Mcc5gAllWifiNonDbs2gStaTest(
+ d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for CUJ MCC with 5G D2D medium and 2G WLAN test."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_mcc = True
+ self.performance_test_iterations = getattr(
+ self.test_mcc_5g_all_wifi_non_dbs_2g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.MCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.MCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_mcc_5g_all_wifi_non_dbs_2g_sta(self):
+ self._test_connection_medium_performance(
+ upgrade_medium_under_test=nc_constants.NearbyMedium.UPGRADE_TO_ALL_WIFI,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+
+ return (
+ f'The upgraded wifi medium {upgraded_medium_name} might be broken, '
+ f'check the related log, Or {self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'{self._throughput_low_string}. The upgraded medium is'
+ f' {upgraded_medium_name} Check with the wifi chip vendor for any FW'
+ ' issue in MCC mode'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g and self.advertiser.supports_5g
+ and not self.advertiser.supports_dbs_sta_wfd
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/cuj_tests/scc_2g_all_wifi_sta_test.py b/tests/bettertogether/betocq/cuj_tests/scc_2g_all_wifi_sta_test.py
new file mode 100644
index 000000000..8d425eb79
--- /dev/null
+++ b/tests/bettertogether/betocq/cuj_tests/scc_2g_all_wifi_sta_test.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC in a general case.
+
+In this case, even though the expected wifi medium is the WFD, but the wifi D2D
+could be any technologies, such as WFD, HOTSPOT, WifiLAN; Once the WFD is
+failed, other meidums will be tried. Both the D2D and STA are using the same 2G
+channel.
+
+The device requirements:
+ support 5G: false
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc2gAllWifiStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC 2G test associated with a specified CUJ."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_2g_d2d_wifi_medium = True
+ self.performance_test_iterations = getattr(
+ self.test_scc_2g_all_wifi_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_2g_all_wifi_sta(self):
+ """Test the 2G SCC case, both the wifi D2D medium and STA are using 2G."""
+ self._test_connection_medium_performance(
+ upgrade_medium_under_test=nc_constants.NearbyMedium.UPGRADE_TO_ALL_WIFI,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password)
+
+ def _get_transfer_file_size(self) -> int:
+ # For 2G wifi medium
+ return nc_constants.TRANSFER_FILE_SIZE_20MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'The upgraded wifi medium {upgraded_medium_name} might be broken, '
+ f'check the related log; Or {self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'{self._throughput_low_string}. The upgraded medium is'
+ f' {upgraded_medium_name}, this is a 2G SCC case. Check with the wifi'
+ ' chip vendor for any FW issue in this mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return not self.discoverer.supports_5g or not self.advertiser.supports_5g
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_dbs_2g_sta_test.py b/tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_dbs_2g_sta_test.py
new file mode 100644
index 000000000..1408b61b4
--- /dev/null
+++ b/tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_dbs_2g_sta_test.py
@@ -0,0 +1,112 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC in a general case.
+
+In this case, even though the expected wifi medium is the WFD, but the wifi D2D
+could be any mediums, such as WFD, HOTSPOT, STA; Once the WFD is failed, other
+meidums will be tried. Also, though the WLAN is connected with 2G channel,
+as the devices support DBS, which don't need to switch between 5G and 2G, it is
+still a SCC case.
+
+The device requirements:
+ support 5G: true
+ support DBS (target device): true
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc5gAllWifiDbs2gStaTest(
+ d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for CUJ SCC with 5G D2D medium and 2G WLAN test.
+ """
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_5g_all_wifi_dbs_2g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_5g_all_wifi_dbs_2g_sta(self):
+ self._test_connection_medium_performance(
+ upgrade_medium_under_test=nc_constants.NearbyMedium.UPGRADE_TO_ALL_WIFI,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'The upgraded wifi medium {upgraded_medium_name} might be broken, '
+ f'check the related log, Or {self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'{self._throughput_low_string}. The upgraded medium is'
+ f' {upgraded_medium_name}. This is a 2G SCC DBS case, In the'
+ ' configuration file, DBS support is set to true on the target side.'
+ ' Check if the device does support DBS with STA + WFD concurrency.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g and self.advertiser.supports_5g
+ and self.advertiser.supports_dbs_sta_wfd
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_sta_test.py b/tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_sta_test.py
new file mode 100644
index 000000000..348d8621a
--- /dev/null
+++ b/tests/bettertogether/betocq/cuj_tests/scc_5g_all_wifi_sta_test.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC in a general case.
+
+In this case, even though the expected wifi medium is the WFD, but the wifi D2D
+could be any technologies, such as WFD, HOTSPOT, STA; Once the WFD is failed,
+other meidums will be tried. Both the D2D medium and STA are using the same 5G
+channel.
+
+The device requirements:
+ support 5G: true
+ country code: US
+The AP requirements:
+ wifi channel: 36 (5180)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc5gAllWifiStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC 5G test associated with a specified CUJ."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_5g_all_wifi_sta_test, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_5g_all_wifi_sta_test(self):
+ self._test_connection_medium_performance(
+ upgrade_medium_under_test=nc_constants.NearbyMedium.UPGRADE_TO_ALL_WIFI,
+ wifi_ssid=self.test_parameters.wifi_5g_ssid,
+ wifi_password=self.test_parameters.wifi_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'The upgraded wifi medium {upgraded_medium_name} might be broken, '
+ f'check the related logs; Or {self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ upgraded_medium_name = None
+ if (self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ is not None):
+ upgraded_medium_name = (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium.name
+ )
+ return (
+ f'{self._throughput_low_string}. The upgraded medium is'
+ f' {upgraded_medium_name}.'
+ ' This is a 5G SCC case. Check with the wifi'
+ ' chip vendor for any possible FW issue.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return self.discoverer.supports_5g and self.advertiser.supports_5g
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/d2d_performance_test_base.py b/tests/bettertogether/betocq/d2d_performance_test_base.py
new file mode 100644
index 000000000..915f2eef6
--- /dev/null
+++ b/tests/bettertogether/betocq/d2d_performance_test_base.py
@@ -0,0 +1,705 @@
+# Copyright (C) 2024 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.
+
+"""Nearby Connection E2E stress tests for D2D wifi performance."""
+
+import abc
+import datetime
+import logging
+import time
+from typing import Any
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+from betocq import nc_base_test
+from betocq import nc_constants
+from betocq import nearby_connection_wrapper
+from betocq import setup_utils
+
+
+_DELAY_BETWEEN_EACH_TEST_CYCLE = datetime.timedelta(seconds=5)
+_INVALID_FREQ = -1
+_INVALID_MAX_LINK_SPEED = -1
+_BITS_PER_BYTE = 8
+
+
+class D2dPerformanceTestBase(nc_base_test.NCBaseTestClass, abc.ABC):
+ """Abstract class for D2D performance test for different connection meidums."""
+
+ performance_test_iterations: int
+
+ # @typing.override
+ def __init__(self, configs):
+ super().__init__(configs)
+ self._is_mcc: bool = False
+ self._is_2g_d2d_wifi_medium: bool = False
+ self._is_dbs_mode: bool = False
+ self._throughput_low_string: str = ''
+ self._upgrade_medium_under_test: nc_constants.NearbyMedium = None
+ self._current_test_result: nc_constants.SingleTestResult = (
+ nc_constants.SingleTestResult()
+ )
+ self._performance_test_metrics: nc_constants.NcPerformanceTestMetrics = (
+ nc_constants.NcPerformanceTestMetrics()
+ )
+ self._prior_bt_nc_fail_reason: nc_constants.SingleTestFailureReason = (
+ nc_constants.SingleTestFailureReason.UNINITIALIZED
+ )
+ self._active_nc_fail_reason: nc_constants.SingleTestFailureReason = (
+ nc_constants.SingleTestFailureReason.UNINITIALIZED
+ )
+ self._finished_test_iteration: int = 0
+ self._use_prior_bt: bool = False
+ self._test_results: list[nc_constants.SingleTestResult] = []
+
+ # @typing.override
+ def setup_test(self):
+ self._current_test_result: nc_constants.SingleTestResult = (
+ nc_constants.SingleTestResult()
+ )
+ self._prior_bt_nc_fail_reason = (
+ nc_constants.SingleTestFailureReason.UNINITIALIZED
+ )
+ self._active_nc_fail_reason = (
+ nc_constants.SingleTestFailureReason.UNINITIALIZED
+ )
+ super().setup_test()
+
+ def teardown_test(self):
+ self._write_current_test_report()
+ self._collect_current_test_metrics()
+ super().teardown_test()
+ time.sleep(_DELAY_BETWEEN_EACH_TEST_CYCLE.total_seconds())
+
+ # @typing.override
+ def _get_skipped_test_class_reason(self) -> str | None:
+ if not self._is_wifi_ap_ready():
+ return 'Wifi AP is not ready for this test.'
+ if not self._are_devices_capabilities_ok():
+ return 'The test is not required per the device capabilities.'
+ return None
+
+ @abc.abstractmethod
+ def _is_wifi_ap_ready(self) -> bool:
+ pass
+
+ @abc.abstractmethod
+ def _are_devices_capabilities_ok(self) -> bool:
+ pass
+
+ def _get_throughout_benchmark(self) -> int:
+ """Gets the throughout benchmark as KBps."""
+ max_num_streams = min(
+ self.discoverer.max_num_streams, self.advertiser.max_num_streams
+ )
+
+ sta_frequency = int(
+ self.advertiser.nearby.wifiGetConnectionInfo().get(
+ 'mFrequency', _INVALID_FREQ
+ )
+ )
+
+ sta_max_link_speed_mbps = int(
+ self.advertiser.nearby.wifiGetConnectionInfo().get(
+ 'mMaxSupportedTxLinkSpeed', _INVALID_MAX_LINK_SPEED
+ )
+ )
+
+ if self._is_2g_d2d_wifi_medium:
+ max_phy_rate_mbps = min(
+ self.discoverer.max_phy_rate_2g_mbps,
+ self.advertiser.max_phy_rate_2g_mbps,
+ )
+ max_phy_rate_mbps = min(
+ max_phy_rate_mbps,
+ max_num_streams * nc_constants.MAX_PHY_RATE_PER_STREAM_N_20_MBPS,
+ )
+ min_throughput_mbyte_per_sec = int(
+ max_phy_rate_mbps
+ * nc_constants.MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_2G
+ / _BITS_PER_BYTE
+ )
+ else: # 5G wifi medium
+ max_phy_rate_mbps = min(
+ self.discoverer.max_phy_rate_5g_mbps,
+ self.advertiser.max_phy_rate_5g_mbps,
+ )
+ # max_num_streams could be smaller in DBS mode
+ if self._is_dbs_mode:
+ max_num_streams = self.advertiser.max_num_streams_dbs
+
+ max_phy_rate_ac80 = (
+ max_num_streams * nc_constants.MAX_PHY_RATE_PER_STREAM_AC_80_MBPS
+ )
+ max_phy_rate_mbps = min(max_phy_rate_mbps, max_phy_rate_ac80)
+
+ # if STA is connected to 5G AP with channel BW < 80,
+ # limit the max phy rate to AC 40.
+ if (sta_frequency > 5000
+ and sta_max_link_speed_mbps > 0
+ and sta_max_link_speed_mbps < max_phy_rate_ac80
+ ):
+ max_phy_rate_mbps = min(
+ max_phy_rate_mbps,
+ max_num_streams * nc_constants.MAX_PHY_RATE_PER_STREAM_AC_40_MBPS,
+ )
+
+ min_throughput_mbyte_per_sec = int(
+ max_phy_rate_mbps
+ * nc_constants.MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_5G
+ / _BITS_PER_BYTE
+ )
+ if self._is_mcc:
+ min_throughput_mbyte_per_sec = int(
+ min_throughput_mbyte_per_sec
+ * nc_constants.MCC_THROUGHPUT_MULTIPLIER
+ )
+ if (
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ == nc_constants.NearbyConnectionMedium.WIFI_HOTSPOT
+ ):
+ min_throughput_mbyte_per_sec = int(
+ min_throughput_mbyte_per_sec
+ * nc_constants.WIFI_HOTSPOT_THROUGHPUT_MULTIPLIER
+ )
+
+ self.advertiser.log.info(
+ f'STA frequency = {sta_frequency}, '
+ f'STA max link speed (Mb/s) = {sta_max_link_speed_mbps}, '
+ f'max D2D phy rate (Mb/s) = {max_phy_rate_mbps}, '
+ f'min D2D throughput (MB/s) = {min_throughput_mbyte_per_sec}'
+ )
+ return min_throughput_mbyte_per_sec * 1024
+
+ def _test_connection_medium_performance(
+ self,
+ upgrade_medium_under_test: nc_constants.NearbyMedium,
+ wifi_ssid: str = '',
+ wifi_password: str = '',
+ force_disable_bt_multiplex: bool = False,
+ connection_medium: nc_constants.NearbyMedium = nc_constants.NearbyMedium.BT_ONLY,
+ ) -> None:
+ """Test the D2D performance with the specified upgrade medium."""
+ self._upgrade_medium_under_test = upgrade_medium_under_test
+
+ if self.test_parameters.toggle_airplane_mode_target_side:
+ setup_utils.toggle_airplane_mode(self.advertiser)
+ if self.test_parameters.reset_wifi_connection:
+ self._reset_wifi_connection()
+ # 1. discoverer connect to wifi STA/AP
+ self._current_test_result = nc_constants.SingleTestResult()
+ if wifi_ssid:
+ self._active_nc_fail_reason = (
+ nc_constants.SingleTestFailureReason.SOURCE_WIFI_CONNECTION
+ )
+ discoverer_wifi_sta_latency = (
+ setup_utils.connect_to_wifi_sta_till_success(
+ self.discoverer, wifi_ssid, wifi_password
+ )
+ )
+ self._active_nc_fail_reason = nc_constants.SingleTestFailureReason.SUCCESS
+ self.discoverer.log.info(
+ 'connecting to wifi in '
+ f'{round(discoverer_wifi_sta_latency.total_seconds())} s'
+ )
+ self._current_test_result.discoverer_sta_expected = True
+ self._current_test_result.discoverer_sta_latency = (
+ discoverer_wifi_sta_latency
+ )
+
+ # 2. set up BT connection if required
+ advertising_discovery_medium = nc_constants.NearbyMedium.BLE_ONLY
+
+ connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts(
+ nc_constants.FIRST_DISCOVERY_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_INIT_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_RESULT_TIMEOUT,
+ )
+ prior_bt_snippet = None
+ if (
+ not force_disable_bt_multiplex
+ and self.test_parameters.requires_bt_multiplex
+ ):
+ logging.info('set up a prior BT connection.')
+ self._use_prior_bt = True
+ prior_bt_snippet = nearby_connection_wrapper.NearbyConnectionWrapper(
+ self.advertiser,
+ self.discoverer,
+ self.advertiser.nearby2,
+ self.discoverer.nearby2,
+ advertising_discovery_medium=advertising_discovery_medium,
+ connection_medium=nc_constants.NearbyMedium.BT_ONLY,
+ upgrade_medium=nc_constants.NearbyMedium.BT_ONLY,
+ )
+
+ try:
+ prior_bt_snippet.start_nearby_connection(
+ timeouts=connection_setup_timeouts,
+ medium_upgrade_type=nc_constants.MediumUpgradeType.NON_DISRUPTIVE,
+ )
+ finally:
+ self._prior_bt_nc_fail_reason = (
+ prior_bt_snippet.test_failure_reason
+ )
+ self._current_test_result.prior_nc_setup_quality_info = (
+ prior_bt_snippet.connection_quality_info
+ )
+
+ # set up Wifi connection and transfer
+ # 3. advertiser connect to wifi STA/AP
+ if wifi_ssid:
+ self._active_nc_fail_reason = (
+ nc_constants.SingleTestFailureReason.TARGET_WIFI_CONNECTION
+ )
+ advertiser_wifi_sta_latency = (
+ setup_utils.connect_to_wifi_sta_till_success(
+ self.advertiser, wifi_ssid, wifi_password
+ )
+ )
+ self.advertiser.log.info(
+ 'connecting to wifi in '
+ f'{round(advertiser_wifi_sta_latency.total_seconds())} s'
+ )
+ self.advertiser.log.info(
+ self.advertiser.nearby.wifiGetConnectionInfo().get('mFrequency')
+ )
+ self._current_test_result.advertiser_wifi_expected = True
+ self._current_test_result.advertiser_sta_latency = (
+ advertiser_wifi_sta_latency
+ )
+
+ # 4. set up the D2D nearby connection
+ logging.info('set up a nearby connection for file transfer.')
+ active_snippet = nearby_connection_wrapper.NearbyConnectionWrapper(
+ self.advertiser,
+ self.discoverer,
+ self.advertiser.nearby,
+ self.discoverer.nearby,
+ advertising_discovery_medium=advertising_discovery_medium,
+ connection_medium=connection_medium,
+ upgrade_medium=upgrade_medium_under_test,
+ )
+ if prior_bt_snippet:
+ connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts(
+ nc_constants.SECOND_DISCOVERY_TIMEOUT,
+ nc_constants.SECOND_CONNECTION_INIT_TIMEOUT,
+ nc_constants.SECOND_CONNECTION_RESULT_TIMEOUT,
+ )
+ try:
+ active_snippet.start_nearby_connection(
+ timeouts=connection_setup_timeouts,
+ medium_upgrade_type=nc_constants.MediumUpgradeType.DISRUPTIVE,
+ keep_alive_timeout_ms=self.test_parameters.keep_alive_timeout_ms,
+ keep_alive_interval_ms=self.test_parameters.keep_alive_interval_ms,
+ )
+ finally:
+ self._active_nc_fail_reason = active_snippet.test_failure_reason
+ self._current_test_result.file_transfer_nc_setup_quality_info = (
+ active_snippet.connection_quality_info
+ )
+
+ # 5. transfer file through the nearby connection
+ try:
+ self._current_test_result.file_transfer_throughput_kbps = (
+ active_snippet.transfer_file(
+ self._get_transfer_file_size(),
+ self._get_file_transfer_timeout(),
+ self.test_parameters.payload_type,
+ )
+ )
+ finally:
+ self._active_nc_fail_reason = active_snippet.test_failure_reason
+ if (
+ self._active_nc_fail_reason
+ is nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ self._throughout_benchmark_kbyte_per_sec = (
+ self._get_throughout_benchmark()
+ )
+
+ if (
+ self._current_test_result.file_transfer_throughput_kbps
+ < self._throughout_benchmark_kbyte_per_sec
+ ):
+ self._active_nc_fail_reason = (
+ nc_constants.SingleTestFailureReason.FILE_TRANSFER_THROUGHPUT_LOW
+ )
+ file_transfer_throughput_kbps = str(
+ self._current_test_result.file_transfer_throughput_kbps
+ )
+ self._throughput_low_string = (
+ 'The measured throughput'
+ f' {file_transfer_throughput_kbps} is'
+ ' lower than the expected'
+ f' {self._throughout_benchmark_kbyte_per_sec} KBps'
+ )
+ asserts.fail(self._throughput_low_string)
+
+ # 6. disconnect prior BT connection if required
+ if prior_bt_snippet:
+ prior_bt_snippet.disconnect_endpoint()
+ # 7. disconnect D2D active connection
+ active_snippet.disconnect_endpoint()
+
+ def _get_transfer_file_size(self) -> int:
+ return nc_constants.TRANSFER_FILE_SIZE_200MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.WIFI_200M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _write_current_test_report(self) -> None:
+ """Writes test report for each iteration."""
+ self._current_test_result.test_iteration = self._finished_test_iteration
+ self._finished_test_iteration += 1
+ if (
+ self._use_prior_bt
+ and self._prior_bt_nc_fail_reason
+ is not nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ self._current_test_result.is_failed_with_prior_bt = True
+ self._current_test_result.failure_reason = self._prior_bt_nc_fail_reason
+ else:
+ self._current_test_result.failure_reason = self._active_nc_fail_reason
+ result_message = self._get_current_test_result_message()
+ self._current_test_result.result_message = result_message
+ self._test_results.append(self._current_test_result)
+
+ quality_info: list[Any] = []
+ if self._use_prior_bt:
+ quality_info.append(
+ 'prior_bt_connection:'
+ f'{self._current_test_result.prior_nc_setup_quality_info.get_dict()}'
+ )
+ quality_info.append(
+ 'file_transfer_connection_quality_info:'
+ f'{self._current_test_result.file_transfer_nc_setup_quality_info.get_dict()}'
+ )
+ quality_info.append(
+ 'file_transfer_speed: '
+ f'{round(self._current_test_result.file_transfer_throughput_kbps/1024, 1)}'
+ 'MBps'
+ )
+
+ if self._current_test_result.discoverer_sta_expected:
+ src_connection_latency = round(
+ self._current_test_result.discoverer_sta_latency.total_seconds()
+ )
+ quality_info.append(f'src_wifi_connection: {src_connection_latency}s')
+ if self._current_test_result.advertiser_wifi_expected:
+ tgt_connection_latency = round(
+ self._current_test_result.advertiser_sta_latency.total_seconds()
+ )
+ quality_info.append(f'tgt_wifi_connection: {tgt_connection_latency}s')
+
+ test_report = {
+ 'result': result_message,
+ 'quality_info': quality_info,
+ }
+
+ self.discoverer.log.info(test_report)
+ self.record_data({
+ 'Test Class': self.TAG,
+ 'Test Name': self.current_test_info.name,
+ 'sponge_properties': test_report,
+ })
+
+ def _get_current_test_result_message(self) -> str:
+ if (
+ self._active_nc_fail_reason
+ == nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ return 'PASS'
+ if (
+ self._active_nc_fail_reason
+ == nc_constants.SingleTestFailureReason.SOURCE_WIFI_CONNECTION
+ ):
+ return ''.join([
+ f'FAIL: {self._active_nc_fail_reason.name} - ',
+ nc_constants.COMMON_TRIAGE_TIP.get(
+ self._active_nc_fail_reason
+ ),
+ ])
+
+ if (self._use_prior_bt and
+ self._prior_bt_nc_fail_reason
+ is not nc_constants.SingleTestFailureReason.SUCCESS):
+ return ''.join([
+ 'FAIL (The prior BT connection): ',
+ f'{self._prior_bt_nc_fail_reason.name} - ',
+ nc_constants.COMMON_TRIAGE_TIP.get(
+ self._prior_bt_nc_fail_reason
+ ),
+ ])
+
+ if (
+ self._active_nc_fail_reason
+ is nc_constants.SingleTestFailureReason.WIFI_MEDIUM_UPGRADE
+ ):
+ return ''.join([
+ f'FAIL: {self._active_nc_fail_reason.name} - ',
+ self._get_medium_upgrade_failure_tip(),
+ ])
+ if (
+ self._active_nc_fail_reason
+ is nc_constants.SingleTestFailureReason.FILE_TRANSFER_FAIL
+ ):
+ return ''.join([
+ f'{self._active_nc_fail_reason.name} - ',
+ self._get_file_transfer_failure_tip(),
+ ])
+ if (
+ self._active_nc_fail_reason
+ is nc_constants.SingleTestFailureReason.FILE_TRANSFER_THROUGHPUT_LOW
+ ):
+ return ''.join([
+ f'{self._active_nc_fail_reason.name} - ',
+ self._get_throughput_low_tip(),
+ ])
+
+ return ''.join([
+ f'{self._active_nc_fail_reason.name} - ',
+ nc_constants.COMMON_TRIAGE_TIP.get(self._active_nc_fail_reason),
+ ])
+
+ def _get_medium_upgrade_failure_tip(self) -> str:
+ return nc_constants.MEDIUM_UPGRADE_FAIL_TRIAGE_TIPS.get(
+ self._upgrade_medium_under_test,
+ f'unexpected upgrade medium - {self._upgrade_medium_under_test}')
+
+ @abc.abstractmethod
+ def _get_file_transfer_failure_tip(self) -> str:
+ pass
+
+ @abc.abstractmethod
+ def _get_throughput_low_tip(self) -> str:
+ pass
+
+ def _collect_current_test_metrics(self) -> None:
+ """Collects test result metrics for each iteration."""
+ if self._use_prior_bt:
+ self._performance_test_metrics.prior_bt_discovery_latencies.append(
+ self._current_test_result.prior_nc_setup_quality_info.discovery_latency
+ )
+ self._performance_test_metrics.prior_bt_connection_latencies.append(
+ self._current_test_result.prior_nc_setup_quality_info.connection_latency
+ )
+
+ self._performance_test_metrics.file_transfer_discovery_latencies.append(
+ self._current_test_result.file_transfer_nc_setup_quality_info.discovery_latency
+ )
+ self._performance_test_metrics.file_transfer_connection_latencies.append(
+ self._current_test_result.file_transfer_nc_setup_quality_info.connection_latency
+ )
+ self._performance_test_metrics.upgraded_wifi_transfer_mediums.append(
+ self._current_test_result.file_transfer_nc_setup_quality_info.upgrade_medium
+ )
+ self._performance_test_metrics.file_transfer_throughputs_kbps.append(
+ self._current_test_result.file_transfer_throughput_kbps
+ )
+ self._performance_test_metrics.discoverer_wifi_sta_latencies.append(
+ self._current_test_result.discoverer_sta_latency
+ )
+ self._performance_test_metrics.advertiser_wifi_sta_latencies.append(
+ self._current_test_result.advertiser_sta_latency
+ )
+ if self._current_test_result.file_transfer_nc_setup_quality_info.medium_upgrade_expected:
+ self._performance_test_metrics.medium_upgrade_latencies.append(
+ self._current_test_result.file_transfer_nc_setup_quality_info.medium_upgrade_latency
+ )
+
+ def __get_median_throughput(
+ self,
+ throughput_indicators: list[float],
+ ) -> tuple[int, int]:
+ """get the median throughput from iterations which finished file transfer."""
+ filtered = [
+ x
+ for x in throughput_indicators
+ if x != nc_constants.UNSET_THROUGHPUT_KBPS
+ ]
+ if not filtered:
+ # all test cases are failed
+ return (0, 0)
+ # use the descenting order of the throughput
+ filtered.sort(reverse=True)
+ return (len(filtered),
+ int(filtered[int(len(filtered) * nc_constants.PERCENTILE_50_FACTOR)]
+ ))
+
+ def __get_median_latency(
+ self, latency_indicators: list[datetime.timedelta]
+ ) -> tuple[int, float]:
+ filtered = [
+ latency.total_seconds()
+ for latency in latency_indicators
+ if latency != nc_constants.UNSET_LATENCY
+ ]
+ if not filtered:
+ # All test cases are failed.
+ return (0, 0.0)
+
+ filtered.sort()
+
+ percentile_50 = round(
+ filtered[int(len(filtered) * nc_constants.PERCENTILE_50_FACTOR)],
+ nc_constants.LATENCY_PRECISION_DIGITS,
+ )
+ return (len(filtered), percentile_50)
+
+ # @typing.override
+ def _summary_test_results(self) -> None:
+ """Summarizes test results of all iterations."""
+ success_count = sum(
+ test_result.failure_reason
+ == nc_constants.SingleTestFailureReason.SUCCESS
+ for test_result in self._test_results
+ )
+ # round down the passing test iterations
+ passed = success_count >= int(
+ self.performance_test_iterations * nc_constants.SUCCESS_RATE_TARGET
+ )
+ final_result_message = (
+ 'PASS' if passed else f'FAIL: low successes - {success_count}'
+ )
+ detailed_stats = [
+ f'Required Iterations: {self.performance_test_iterations}',
+ f'Finished Iterations: {len(self._test_results)}']
+ detailed_stats.append('Failed Iterations:')
+ detailed_stats.extend(self.__get_failed_iteration_messages())
+ detailed_stats.append('File Transfer Connection Stats:')
+ detailed_stats.extend(self.__get_file_transfer_connection_stats())
+
+ if self._use_prior_bt:
+ detailed_stats.append('Prior BT Connection Stats:')
+ detailed_stats.extend(self.__get_prior_bt_connection_stats())
+
+ self.record_data({
+ 'Test Class': self.TAG,
+ 'sponge_properties': {
+ '01_test_result': final_result_message,
+ '02_source_device': '\n'.join(
+ self.__get_device_attributes(self.discoverer)
+ ),
+ '03_target_device': '\n'.join(
+ self.__get_device_attributes(self.advertiser)
+ ),
+ '04_detailed_stats': '\n'.join(detailed_stats),
+ },
+ })
+
+ asserts.assert_true(passed, final_result_message)
+
+ def __get_failed_iteration_messages(self) -> list[str]:
+ stats = []
+ for test_result in self._test_results:
+ if (
+ test_result.failure_reason
+ is not nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ stats.append(f' - {test_result.test_iteration}: '
+ f'{test_result.result_message}')
+
+ if stats:
+ return stats
+ else:
+ return [' - NA']
+
+ def __get_prior_bt_connection_stats(self) -> list[str]:
+ if not self._use_prior_bt:
+ return []
+ (prior_bt_discovery_successes, prior_bt_discovery_median_latency) = (
+ self.__get_median_latency(
+ self._performance_test_metrics.prior_bt_discovery_latencies
+ )
+ )
+ (prior_bt_connection_successes, prior_bt_connection_median_latency) = (
+ self.__get_median_latency(
+ self._performance_test_metrics.prior_bt_connection_latencies
+ )
+ )
+ return [
+ f' - Median Discovery Latency ({prior_bt_discovery_successes}): '
+ f'{prior_bt_discovery_median_latency}s',
+ f' - Median Connection Latency ({prior_bt_connection_successes}): '
+ f'{prior_bt_connection_median_latency}s',
+ ]
+
+ def __get_file_transfer_connection_stats(self) -> list[str]:
+ (active_discovery_successes, active_discovery_median_latency) = (
+ self.__get_median_latency(
+ self._performance_test_metrics.file_transfer_discovery_latencies)
+ )
+ (active_connection_successes, active_connection_median_latency) = (
+ self.__get_median_latency(
+ self._performance_test_metrics.file_transfer_connection_latencies)
+ )
+ (file_transfer_successes, file_transfer_median_throughput) = (
+ self.__get_median_throughput(
+ self._performance_test_metrics.file_transfer_throughputs_kbps)
+ )
+ stats = [
+ f' - Median Discovery Latency ({active_discovery_successes}): '
+ f'{active_discovery_median_latency}s',
+ f' - Median Connection Latency ({active_connection_successes}): '
+ f'{active_connection_median_latency}s',
+ f' - Median File Transfer Speed ({file_transfer_successes}): '
+ f'{round(file_transfer_median_throughput/1024, 1)}MBps',
+ ]
+ if nc_constants.is_high_quality_medium(self._upgrade_medium_under_test):
+ (medium_upgrade_successes, medium_upgrade_median_latency) = (
+ self.__get_median_latency(
+ self._performance_test_metrics.medium_upgrade_latencies)
+ )
+ stats.extend([
+ f' - Median Upgrade Latency ({medium_upgrade_successes}): '
+ f'{medium_upgrade_median_latency}s',
+ ' - Upgrade Medium Stats:'
+ ])
+ stats.extend(self._summary_upgraded_wifi_transfer_mediums())
+
+ return stats
+
+ def _summary_upgraded_wifi_transfer_mediums(self) -> list[str]:
+ medium_counts = {}
+ for (
+ upgraded_medium
+ ) in self._performance_test_metrics.upgraded_wifi_transfer_mediums:
+ if upgraded_medium:
+ medium_counts[upgraded_medium.name] = (
+ medium_counts.get(upgraded_medium.name, 0) + 1
+ )
+ return [f' - {name}: {count}' for name, count in medium_counts.items()]
+
+ def __get_device_attributes(
+ self, ad: android_device.AndroidDevice
+ ) -> list[str]:
+ return [
+ f'Device Serial: {ad.serial}',
+ f'Device Model: {ad.model}',
+ f'Supports 5G Wifi: {ad.supports_5g}',
+ f'Supports DBS: {ad.supports_dbs_sta_wfd}',
+ (
+ 'Enable STA DFS channel for peer network:'
+ f' {ad.enable_sta_dfs_channel_for_peer_network}'
+ ),
+ (
+ 'Enable STA Indoor channel for peer network:'
+ f' {ad.enable_sta_indoor_channel_for_peer_network}'
+ ),
+ f'Max num of streams: {ad.max_num_streams}',
+ f'Max num of streams (DBS): {ad.max_num_streams_dbs}',
+ f'Android Version: {ad.android_version}',
+ f'GMS_version: {setup_utils.dump_gms_version(ad)}',
+ ]
diff --git a/tests/bettertogether/betocq/directed_tests/ble_performance_test.py b/tests/bettertogether/betocq/directed_tests/ble_performance_test.py
new file mode 100644
index 000000000..8db2f10b7
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/ble_performance_test.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the BLE performance."""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class BlePerformanceTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for the BLE connection performance."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_ble_performance, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.BT_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.BT_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_ble_performance(self):
+ """Test the performance of the BLE."""
+ self._test_connection_medium_performance(
+ upgrade_medium_under_test=nc_constants.NearbyMedium.BLE_ONLY,
+ force_disable_bt_multiplex=True,
+ connection_medium=nc_constants.NearbyMedium.BLE_ONLY,
+ )
+
+ def _get_transfer_file_size(self) -> int:
+ return nc_constants.TRANSFER_FILE_SIZE_1MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.BLE_1M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_throughout_benchmark(self) -> int:
+ return nc_constants.BLE_MEDIUM_THROUGHPUT_BENCHMARK
+
+ def _get_medium_upgrade_failure_tip(self) -> str:
+ return 'Not Applied' # No medium upgrade required for BLE.
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The BLE connection might be broken, check the related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. Check with the chip vendor if there is'
+ ' any BT firmware issue.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ # don't require wifi STA.
+ return True
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ # no special capabilities is required.
+ return True
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/bt_performance_test.py b/tests/bettertogether/betocq/directed_tests/bt_performance_test.py
new file mode 100644
index 000000000..689e001b7
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/bt_performance_test.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the classic Bluetooth performance."""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class BtPerformanceTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for the classic Bluetooth connection performance."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_classic_bt_performance, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.BT_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.BT_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_classic_bt_performance(self):
+ """Test the performance of the classic BT connetion."""
+ self._test_connection_medium_performance(
+ upgrade_medium_under_test=nc_constants.NearbyMedium.BT_ONLY,
+ force_disable_bt_multiplex=True
+ )
+
+ def _get_transfer_file_size(self) -> int:
+ return nc_constants.TRANSFER_FILE_SIZE_1MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.BT_1M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_throughout_benchmark(self) -> int:
+ return nc_constants.CLASSIC_BT_MEDIUM_THROUGHPUT_BENCHMARK
+
+ def _get_medium_upgrade_failure_tip(self) -> str:
+ return 'Not Applied' # No medium upgrade required for BT.
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The classic Bluetooth connection might be broken, check related log, '
+ f' {self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. Check with the chip vendor if there is'
+ ' any BT firmware issue.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ # don't require wifi STA.
+ return True
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ # no special capabilities is required.
+ return True
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/local_dev_directed_testbed.yml b/tests/bettertogether/betocq/directed_tests/local_dev_directed_testbed.yml
new file mode 100644
index 000000000..ea3e03016
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/local_dev_directed_testbed.yml
@@ -0,0 +1,77 @@
+x-test-params: &test-params
+ target_cuj_name: "quick_start"
+ allow_unrooted_device: False
+ wifi_2g_ssid: "NETGEAR62"
+ wifi_2g_password: "widetree588"
+ wifi_5g_ssid: "NETGEAR62-5G-1"
+ wifi_5g_password: "widetree588"
+ wifi_dfs_5g_ssid: "GoogleGuest"
+ wifi_dfs_5g_password: ""
+ # use the AP controlled by programming dynamically
+ use_auto_controlled_wifi_ap: False
+
+x-controllers: &controllers
+ Controllers:
+ AndroidDevice:
+ - serial: "R3CN90YNAR" # "R3CN90YNAR"
+ role: "source_device"
+ # The max number of spatial streams
+ max_num_streams: 2
+ # The max PHY rate at 5G, in Mbps
+ max_phy_rate_5g_mbps: 2402
+ # The max PHY rate at 2G, in Mbps
+ max_phy_rate_2g_mbps: 287
+ # if the device supports 5G Wifi
+ supports_5g: True # True
+ # if the device supports DBS (Dual Band Simultaneous) in STA + WiFi-Direct concurrency mode
+ supports_dbs_sta_wfd: True
+ # The max number of spatial streams in DBS mode.
+ max_num_streams_dbs: 2
+ # if the device supports to start WFD group owner at a STA-associated DFS channel
+ enable_sta_dfs_channel_for_peer_network: False
+ # if the device supports to start WFD group owner at a STA-associated indoor channel
+ enable_sta_indoor_channel_for_peer_network: False
+ # 14 - U, 13 - T, 12 - S
+ android_version: 14
+ - serial: "2B031FDJH0002G" # "ABCDEF123456"
+ role: "target_device"
+ # The max number of spatial streams
+ max_num_streams: 2
+ # The max PHY rate at 5G, in Mbps
+ max_phy_rate_5g_mbps: 2402
+ # The max PHY rate at 2G, in Mbps
+ max_phy_rate_2g_mbps: 287
+ # if the device supports 5G Wifi
+ supports_5g: True # True
+ # if the device supports DBS (Dual Band Simultaneous) in STA + WiFi-Direct concurrency mode
+ supports_dbs_sta_wfd: True
+ # The max number of spatial streams in DBS mode.
+ max_num_streams_dbs: 2
+ # if the device supports to start WFD group owner at a STA-associated DFS channel
+ enable_sta_dfs_channel_for_peer_network: True
+ # if the device supports to start WFD group owner at a STA-associated indoor channel
+ enable_sta_indoor_channel_for_peer_network: True
+ # 14 - U, 13 - T, 12 - S
+ android_version: 14
+
+TestBeds:
+- Name: LocalPerformanceDefaultTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+
+- Name: LocalQuickstartPerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_start"
+ # before the performance test, run the function tests first
+ run_function_tests_with_performance_tests: True
+ # if the function tests is failed, abort the performance test
+ abort_all_tests_on_function_tests_fail: True
+
+- Name: LocalQuicksharePerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_share"
diff --git a/tests/bettertogether/betocq/directed_tests/mcc_2g_wfd_indoor_5g_sta_test.py b/tests/bettertogether/betocq/directed_tests/mcc_2g_wfd_indoor_5g_sta_test.py
new file mode 100644
index 000000000..6a263a9a3
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/mcc_2g_wfd_indoor_5g_sta_test.py
@@ -0,0 +1,111 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi MCC with the indoor channels case.
+
+This is about the feature - using indoor channels for WFD, for details, refer to
+https://docs.google.com/presentation/d/18Fl0fY4piq_sfXfo3rCr2Ca55AJHEOvB7rC-rV3SQ9E/edit?usp=sharing
+and config_wifiEnableStaIndoorChannelForPeerNetwork -
+https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1147
+In this case, the feature is disable for the device; The WFD will be started on
+a 2G channel, but the STA is using the 5G channel.
+
+The device requirements:
+ support 5G: true
+ using indoor channels for peer network: false
+The AP requirements:
+ wifi channel: 36 (5180)
+"""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Mcc2gWfdIndoor5gStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for wifi MCC with 2G WFD and indoor 5G STA."""
+
+ def _get_country_code(self) -> str:
+ return 'JP'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_mcc = True
+ self._is_2g_d2d_wifi_medium = True
+ self.performance_test_iterations = getattr(
+ self.test_mcc_2g_wfd_indoor_5g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_mcc_2g_wfd_indoor_5g_sta(self):
+ """Test the performance for wifi MCC with 2G WFD and indoor 5G STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_5g_ssid,
+ wifi_password=self.test_parameters.wifi_5g_password,
+ )
+
+ def _get_transfer_file_size(self) -> int:
+ # For 2G wifi medium
+ return nc_constants.TRANSFER_FILE_SIZE_20MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. This is a MCC test case where WFD uses'
+ ' a 2G channel and the STA uses a 5G indoor channel. Check with the'
+ ' wifi chip vendor about the possible firmware Tx/Rx issues in MCC'
+ ' mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g
+ and self.advertiser.supports_5g
+ and not self.advertiser.enable_sta_indoor_channel_for_peer_network
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/mcc_5g_hotspot_dfs_5g_sta_test.py b/tests/bettertogether/betocq/directed_tests/mcc_5g_hotspot_dfs_5g_sta_test.py
new file mode 100644
index 000000000..93f7bee6d
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/mcc_5g_hotspot_dfs_5g_sta_test.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi MCC with the DFS channels case.
+
+This is about the feature - using DFS channels for Hotspot, for details, refer
+to
+https://docs.google.com/presentation/d/18Fl0fY4piq_sfXfo3rCr2Ca55AJHEOvB7rC-rV3SQ9E/edit?usp=sharing
+andconfig_wifiEnableStaDfsChannelForPeerNetwork -
+https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1151
+In this case, the feature is disable for the device; The WLAN is using the DFS
+5G channel, but the hotspot will be started on another non DFS 5G channel.
+
+The device requirements:
+ support 5G: true
+ using DFS channels for peer network (target device): false
+The AP requirements:
+ wifi channel: 52 (5260)
+ TODO(kaishi): verify DFS channel.
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Mcc5gHotspotDfs5gStaTest(
+ d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for MCC with 5G HOTSPOT and DFS 5G STA."""
+
+ def _get_country_code(self) -> str:
+ return 'GB'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_mcc = True
+ self.performance_test_iterations = getattr(
+ self.test_mcc_5g_hotspot_dfs_5g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.MCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.MCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_mcc_5g_hotspot_dfs_5g_sta(self):
+ """Test the performance for wifi MCC with 5G HOTSPOT and DFS 5G STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIHOTSPOT,
+ wifi_ssid=self.test_parameters.wifi_dfs_5g_ssid,
+ wifi_password=self.test_parameters.wifi_dfs_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The hotspot connection might be broken, check the related log, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. This is a MCC test case where hotspot'
+ ' uses a 5G non-DFS channel and STA uses a5G DFS channel. Note that in'
+ ' hotspot mode, the target acts as a WFD GOwhile the source device'
+ ' acts as the legcy STA. Check with the wifi chip vendorabout the'
+ ' possible firmware Tx/Rx issues in MCC mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_dfs_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g
+ and self.advertiser.supports_5g
+ and not self.advertiser.enable_sta_dfs_channel_for_peer_network
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_dfs_5g_sta_test.py b/tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_dfs_5g_sta_test.py
new file mode 100644
index 000000000..bf3444060
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_dfs_5g_sta_test.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi MCC with the DFS channels case.
+
+This is about the feature - using DFS channels for WFD, for details, refer to
+https://docs.google.com/presentation/d/18Fl0fY4piq_sfXfo3rCr2Ca55AJHEOvB7rC-rV3SQ9E/edit?usp=sharing
+and config_wifiEnableStaDfsChannelForPeerNetwork -
+https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1151
+In this case, the feature is disable for the device; The STA is using the DFS 5G
+channel, but the WFD will be started on another 5G channel.
+
+The device requirements:
+ support 5G: true
+ using DFS channels for peer network: false
+The AP requirements:
+ wifi channel: 52 (5260)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Mcc5gWfdDfs5gStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for MCC with 5G WFD and DFS 5G STA."""
+
+ def _get_country_code(self) -> str:
+ return 'GB'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_mcc = True
+ self.performance_test_iterations = getattr(
+ self.test_mcc_5g_wfd_dfs_5g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.MCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.MCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_mcc_5g_wfd_dfs_5g_sta(self):
+ """Test the performance for wifi MCC with 5G WFD and DFS 5G STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_dfs_5g_ssid,
+ wifi_password=self.test_parameters.wifi_dfs_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check the related log, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ 'f{self._throughput_low_string}. This is a MCC test case where WFD uses'
+ ' a 5G non-DFS channel and STA uses a5G DFS channel. Check with the'
+ ' wifi chip vendorabout the possible firmware Tx/Rx issues in MCC mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_dfs_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g
+ and self.advertiser.supports_5g
+ and not self.advertiser.enable_sta_dfs_channel_for_peer_network
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_non_dbs_2g_sta_test.py b/tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_non_dbs_2g_sta_test.py
new file mode 100644
index 000000000..2ab994988
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/mcc_5g_wfd_non_dbs_2g_sta_test.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi MCC in a general case.
+
+In this case, the WFD is using the 5G channel, but STA is connected to 2G
+channel, as the device(don't support DBS) can not handle the 5G and 2G at
+the same time, there is concurrent contention for the 5G channel and 2G
+channel handling in firmware, the firmware needs to switch 5G and 2G from time
+to time.
+
+The device requirements:
+ support 5G: true
+ support DBS(Target Device): False
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Mcc5gWfdNonDbs2gStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for MCC case with 5G WFD and 2G STA."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_mcc = True
+ self.performance_test_iterations = getattr(
+ self.test_mcc_5g_wfd_non_dbs_2g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.MCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.MCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_mcc_5g_wfd_non_dbs_2g_sta(self):
+ """Test the performance for wifi MCC with 5G WFD and 2G STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}.'
+ 'This is a MCC test case where WFD uses a 5G channel and STA uses a'
+ '2G channel. Check with the wifi chip vendor'
+ 'about the possible firmware Tx/Rx issues in MCC mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g and self.advertiser.supports_5g
+ and not self.advertiser.supports_dbs_sta_wfd
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_2g_wfd_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_2g_wfd_sta_test.py
new file mode 100644
index 000000000..7bb56f1fa
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_2g_wfd_sta_test.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC in a general case.
+
+In this case, both the WFD and WLAN are using the 2G channel, the WFD and WLAN
+should use the same channel.
+
+The device requirements:
+ support 5G: false
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc2gWfdStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC with 2G WFD and STA."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_2g_d2d_wifi_medium = True
+ self.performance_test_iterations = getattr(
+ self.test_scc_2g_wfd_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_2g_wfd_sta(self):
+ """Test the performance for Wifi SCC with 2G WFD and STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_transfer_file_size(self) -> int:
+ # For 2G wifi medium
+ return nc_constants.TRANSFER_FILE_SIZE_20MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ 'f{self._throughput_low_string}.'
+ 'This is a SCC 2G test case with WFD medium. Check with the wifi chip'
+ 'vendor about the possible firmware Tx/Rx issues in this mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return not self.discoverer.supports_5g or not self.advertiser.supports_5g
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_2g_wlan_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_2g_wlan_sta_test.py
new file mode 100644
index 000000000..8e3dccc97
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_2g_wlan_sta_test.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi LAN 2G SCC in a general case.
+
+In this case, both the Wifi LAN and STA(internet AP) are using the 2G channel,
+the Wifi LAN and STA should use the same channel.
+Wifi LAN - The target device starts WFD GO as the role of AP, the source device
+is connected to the AP.
+
+The device requirements:
+ support 5G: false
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import datetime
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc2gWlanStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC with 2G WifiLAN and STA."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_2g_d2d_wifi_medium = True
+ self.performance_test_iterations = getattr(
+ self.test_scc_2g_wifilan_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_2g_wifilan_sta(self):
+ """Test the performance for Wifi SCC with 2G WifiLAN and STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.WIFILAN_ONLY,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_transfer_file_size(self) -> int:
+ # For 2G wifi medium
+ return nc_constants.TRANSFER_FILE_SIZE_20MB
+
+ def _get_file_transfer_timeout(self) -> datetime.timedelta:
+ return nc_constants.WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The WLAN connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}.'
+ 'This is a SCC 2G test case with WLAN medium. Check with the wifi chip'
+ 'vendor if TDLS is supported correctly. Also check if'
+ 'the AP has the firewall which could block the mDNS traffic.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return not self.discoverer.supports_5g or not self.advertiser.supports_5g
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_5g_wfd_dbs_2g_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_5g_wfd_dbs_2g_sta_test.py
new file mode 100644
index 000000000..1e7b50517
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_5g_wfd_dbs_2g_sta_test.py
@@ -0,0 +1,99 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi MCC in a general case.
+
+In this case, even though the STA is connected to a 2G channel, and the WFD is
+using the 5G channel, as both devices support DBS, which can handle the 5G and
+2G at the same time, this is still a SCC case.
+
+The device requirements:
+ support 5G: true
+ support DBS(target): true
+The AP requirements:
+ wifi channel: 6 (2437)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc5gWfdDbs2gStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for SCC with 5G WFD and 2G STA."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self._is_dbs_mode = True
+ self.performance_test_iterations = getattr(
+ self.test_scc_5g_wfd_dbs_2g_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_5g_wfd_dbs_2g_sta(self):
+ """Test the performance for wifi SCC with 5G WFD and 2G STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_2g_ssid,
+ wifi_password=self.test_parameters.wifi_2g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. This is a SCC 5G test case with WFD'
+ ' medium operating at 5G and STA operatingat 2G. In the configuration'
+ ' file, DBS support is set to true. Check if thedevice does support'
+ ' DBS with STA + WFD concurrency. Check with the wifi chipvendor about'
+ ' the possible firmware Tx/Rx issues in this mode.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_2g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g and self.advertiser.supports_5g
+ and self.advertiser.supports_dbs_sta_wfd
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_5g_wfd_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_5g_wfd_sta_test.py
new file mode 100644
index 000000000..7bc46b83f
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_5g_wfd_sta_test.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC in a general case.
+
+In this case, both the WFD and WLAN are using the 5G channel, the WFD and WLAN
+should use the same channel.
+
+The device requirements:
+ support 5G: true
+The AP requirements:
+ wifi channel: 36 (5180)
+"""
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc5gWfdStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC with 5G WFD and STA."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_5g_wfd_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_5g_wfd_sta(self):
+ """Test the performance for Wifi SCC with 5G WFD and STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_5g_ssid,
+ wifi_password=self.test_parameters.wifi_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ 'f{self._throughput_low_string}. This is a SCC 5G test case with WFD'
+ ' and STA operating at the same 5G channel.Check with the wifi chip'
+ ' vendor about the possible firmware Tx/Rx issues inthis mode. Also'
+ ' check if the AP channel is set correctly and is supported bythe used'
+ ' wifi medium.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return self.discoverer.supports_5g and self.advertiser.supports_5g
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_5g_wlan_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_5g_wlan_sta_test.py
new file mode 100644
index 000000000..14cec0bff
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_5g_wlan_sta_test.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC of LAN in a general case.
+
+In this case, both the D2D_LAN and WLAN are using the 5G channel, the D2d_LAN
+and WLAN should use the same channel.
+
+The device requirements:
+ support 5G: true
+The AP requirements:
+ wifi channel: 36 (5180)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class Scc5gWifiLanStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC with 5G WifiLAN and STA."""
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_5g_wifilan_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_5g_wifilan_sta(self):
+ """Test the performance for Wifi SCC with 5G WifiLAN and STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.WIFILAN_ONLY,
+ wifi_ssid=self.test_parameters.wifi_5g_ssid,
+ wifi_password=self.test_parameters.wifi_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The WLAN connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. This is 5G WLAN test case. Check with'
+ ' the wifi chip vendor if TDLS issupported correctly. Also check if'
+ ' the AP has the firewall which could blockthe mDNS traffic.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return self.discoverer.supports_5g and self.advertiser.supports_5g
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_dfs_5g_hotspot_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_dfs_5g_hotspot_sta_test.py
new file mode 100644
index 000000000..36599c8f4
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_dfs_5g_hotspot_sta_test.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC with the DFS channels case.
+
+This is about the feature - using DFS channels for Hotspot, for details, refer
+to
+https://docs.google.com/presentation/d/18Fl0fY4piq_sfXfo3rCr2Ca55AJHEOvB7rC-rV3SQ9E/edit?usp=sharing
+and config_wifiEnableStaDfsChannelForPeerNetwork -
+https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1151
+In this case, the feature is enabled for the target device.
+
+The device requirements:
+ support 5G: true
+ using DFS channels for peer network(target device): true
+The AP requirements:
+ wifi channel: 52 (5260)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class SccDfs5gHotspotStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for SCC with Hotspot on a 5G DFS channel."""
+
+ def _get_country_code(self) -> str:
+ return 'GB'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_dfs_5g_hotspot_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_dfs_5g_hotspot_sta(self):
+ """Test the performance for wifi SCC with DFS 5G Hotspot and STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIHOTSPOT,
+ wifi_ssid=self.test_parameters.wifi_dfs_5g_ssid,
+ wifi_password=self.test_parameters.wifi_dfs_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Hotspot connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ f'{self._throughput_low_string}. This is 5G SCC DFS hotspot test case.'
+ ' In the configuration fileenable_sta_dfs_channel_for_peer_network is'
+ ' set to true.Check if the target device does support WFD group owner'
+ ' in the STA-associatedDFS channel. Check if'
+ ' config_wifiEnableStaDfsChannelForPeerNetworkhttps://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1151)is'
+ ' set to true and has the correct driver/FW implementation.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g
+ and self.advertiser.supports_5g
+ and self.advertiser.enable_sta_dfs_channel_for_peer_network
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_dfs_5g_wfd_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_dfs_5g_wfd_sta_test.py
new file mode 100644
index 000000000..eb281ef00
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_dfs_5g_wfd_sta_test.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC with the DFS channels case.
+
+This is about the feature - using DFS channels for WFD, for details, refer to
+https://docs.google.com/presentation/d/18Fl0fY4piq_sfXfo3rCr2Ca55AJHEOvB7rC-rV3SQ9E/edit?usp=sharing
+and config_wifiEnableStaDfsChannelForPeerNetwork -
+https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1151
+In this case, the feature is enabled for the target device.
+
+The device requirements:
+ support 5G: true
+ using DFS channels for peer network: true
+The AP requirements:
+ wifi channel: 52 (5260)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class SccDfs5gWfdStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """Test class for Wifi SCC with the DFS 5G channels case."""
+
+ def _get_country_code(self) -> str:
+ return 'GB'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_dfs_5g_wfd_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_dfs_5g_wfd_sta(self):
+ """Test the performance for Wifi SCC with DFS 5G WFD and STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_dfs_5g_ssid,
+ wifi_password=self.test_parameters.wifi_dfs_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ 'f{self._throughput_low_string}. This is 5G SCC DFS WFD test case. In'
+ ' the configuration file,enable_sta_dfs_channel_for_peer_network is set'
+ ' to true for both src/target.Check if both device do support WFD group'
+ ' owner in the STA-associatedDFS channel. Check if'
+ ' config_wifiEnableStaDfsChannelForPeerNetworkhttps://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1151),is'
+ ' set to true and has the correct driver/FW implementation.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g
+ and self.advertiser.supports_5g
+ and self.discoverer.enable_sta_dfs_channel_for_peer_network
+ and self.advertiser.enable_sta_dfs_channel_for_peer_network
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/directed_tests/scc_indoor_5g_wfd_sta_test.py b/tests/bettertogether/betocq/directed_tests/scc_indoor_5g_wfd_sta_test.py
new file mode 100644
index 000000000..71d163e77
--- /dev/null
+++ b/tests/bettertogether/betocq/directed_tests/scc_indoor_5g_wfd_sta_test.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2024 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.
+
+"""This Test is to test the Wifi SCC with the indoor channels case.
+
+This is about the feature - using indoor channels for WFD, for details, refer to
+https://docs.google.com/presentation/d/18Fl0fY4piq_sfXfo3rCr2Ca55AJHEOvB7rC-rV3SQ9E/edit?usp=sharing
+and config_wifiEnableStaIndoorChannelForPeerNetwork -
+https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1147
+In this case, the feature is enabled on both devices; the WFD and STA both are
+operating on the same 5G indoor channel.
+
+The device requirements:
+ support 5G: true
+ using indoor channels for WFD: true
+The AP requirements:
+ wifi channel: 36 (5180)
+"""
+
+import logging
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import base_test
+from mobly import test_runner
+
+from betocq import d2d_performance_test_base
+from betocq import nc_constants
+
+
+class SccIndoor5gWfdStaTest(d2d_performance_test_base.D2dPerformanceTestBase):
+ """This class is to test the Wifi SCC with the indoor channel case."""
+
+ def _get_country_code(self) -> str:
+ return 'JP'
+
+ def setup_class(self):
+ super().setup_class()
+ self.performance_test_iterations = getattr(
+ self.test_scc_indoor_5g_wfd_sta, base_test.ATTR_REPEAT_CNT
+ )
+ logging.info(
+ 'performance test iterations: %s', self.performance_test_iterations
+ )
+
+ @base_test.repeat(
+ count=nc_constants.SCC_PERFORMANCE_TEST_COUNT,
+ max_consecutive_error=nc_constants.SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR,
+ )
+ def test_scc_indoor_5g_wfd_sta(self):
+ """Test the performance for Wifi SCC with 5G indoor WFD and 5G STA."""
+ self._test_connection_medium_performance(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ wifi_ssid=self.test_parameters.wifi_5g_ssid,
+ wifi_password=self.test_parameters.wifi_5g_password,
+ )
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ return (
+ 'The Wifi Direct connection might be broken, check related logs, '
+ f'{self._get_throughput_low_tip()}'
+ )
+
+ def _get_throughput_low_tip(self) -> str:
+ return (
+ 'f{self._throughput_low_string}. This is 5G SCC indoor WFD test case.'
+ ' In the configuration file,enable_sta_indoor_channel_for_peer_network'
+ ' is set to true.Check if the target device does support WFD group'
+ ' owner in the STA-associatedindoor channel. Check if'
+ ' config_wifiEnableStaIndoorChannelForPeerNetworkhttps://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml;l=1147),is'
+ ' set to true and has the correct driver/FW implementation.'
+ )
+
+ def _is_wifi_ap_ready(self) -> bool:
+ return True if self.test_parameters.wifi_5g_ssid else False
+
+ def _are_devices_capabilities_ok(self) -> bool:
+ return (
+ self.discoverer.supports_5g
+ and self.advertiser.supports_5g
+ and self.advertiser.enable_sta_indoor_channel_for_peer_network
+ )
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/function_tests/beto_cq_function_group_test.py b/tests/bettertogether/betocq/function_tests/beto_cq_function_group_test.py
new file mode 100644
index 000000000..656ed95cc
--- /dev/null
+++ b/tests/bettertogether/betocq/function_tests/beto_cq_function_group_test.py
@@ -0,0 +1,139 @@
+# Copyright (C) 2024 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.
+
+"""Group all function tests."""
+
+import os
+import sys
+
+# Allows local imports to be resolved via relative path, so the test can be run
+# without building.
+_betocq_dir = os.path.dirname(os.path.dirname(__file__))
+if _betocq_dir not in sys.path:
+ sys.path.append(_betocq_dir)
+
+from mobly import test_runner
+
+from betocq import nc_base_test
+from betocq import nc_constants
+from betocq import setup_utils
+from betocq import version
+from betocq.function_tests import bt_ble_function_test_actor
+from betocq.function_tests import bt_multiplex_function_test_actor
+from betocq.function_tests import fixed_wifi_medium_function_test_actor
+from betocq.function_tests import function_test_actor_base
+
+
+class BetoCqFunctionGroupTest(nc_base_test.NCBaseTestClass):
+ """The test class to group all function tests in one mobly test."""
+
+ def __init__(self, configs):
+ super().__init__(configs)
+
+ self._test_result_messages: dict[str, str] = {}
+
+ def test_bt_ble_function(self):
+ """Test the NC with the BT/BLE medium only."""
+ self._current_test_actor = self.bt_ble_test_actor
+ self.bt_ble_test_actor.test_bt_ble_connection()
+
+ def test_wifilan_function(self):
+ """Test the NC with upgrading to the Wifi LAN medium.
+
+ step 1: connect to wifi
+ step 2: set up a nearby connection with the WifiLAN medium and transfer a
+ small file.
+ """
+ self._current_test_actor = self.fixed_wifi_medium_test_actor
+ self.fixed_wifi_medium_test_actor.connect_to_wifi()
+ self.fixed_wifi_medium_test_actor.run_fixed_wifi_medium_test(
+ nc_constants.NearbyMedium.WIFILAN_ONLY)
+
+ def test_d2d_hotspot_function(self):
+ """Test the NC with upgrading to the HOTSPOT as connection medium.
+ """
+ self._current_test_actor = self.fixed_wifi_medium_test_actor
+ self.fixed_wifi_medium_test_actor.run_fixed_wifi_medium_test(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIHOTSPOT)
+
+ def test_wifi_direct_function(self):
+ """Test the NC with upgrading to the WiFi Direct as connection medium.
+ """
+ self._current_test_actor = self.fixed_wifi_medium_test_actor
+ self.fixed_wifi_medium_test_actor.run_fixed_wifi_medium_test(
+ nc_constants.NearbyMedium.UPGRADE_TO_WIFIDIRECT)
+
+ def test_bt_multiplex_connections(self):
+ """Test the BT multiplex function of nearby connection.
+
+ set up 2 Bluetooth connections with NearbyConnection APIs.
+ """
+ self._current_test_actor = self.bt_multiplex_test_actor
+ self.bt_multiplex_test_actor.test_bt_multiplex_connections()
+
+ def setup_class(self):
+ super().setup_class()
+
+ self.bt_ble_test_actor = bt_ble_function_test_actor.BtBleFunctionTestActor(
+ self.test_parameters, self.discoverer, self.advertiser
+ )
+ self.fixed_wifi_medium_test_actor = (
+ fixed_wifi_medium_function_test_actor.FixedWifiMediumFunctionTestActor(
+ self.test_parameters, self.discoverer, self.advertiser
+ )
+ )
+ self.bt_multiplex_test_actor = (
+ bt_multiplex_function_test_actor.BtMultiplexFunctionTestActor(
+ self.test_parameters, self.discoverer, self.advertiser
+ )
+ )
+ self._current_test_actor: function_test_actor_base.FunctionTestActorBase = (
+ None
+ )
+
+ def teardown_test(self) -> None:
+ self._test_result_messages[self.current_test_info.name] = (
+ self._current_test_actor.get_test_result_message()
+ )
+ self.record_data({
+ 'Test Name': self.current_test_info.name,
+ 'sponge_properties': {
+ 'result': self._current_test_actor.get_test_result_message(),
+ },
+ })
+ super().teardown_test()
+
+ # @typing.override
+ def _summary_test_results(self):
+ """Summarizes test results of all function tests."""
+
+ self.record_data({
+ 'Test Class': self.TAG,
+ 'sponge_properties': {
+ '00_test_script_verion': version.TEST_SCRIPT_VERSION,
+ '01_source_device_serial': self.discoverer.serial,
+ '02_target_device_serial': self.advertiser.serial,
+ '03_source_GMS_version': setup_utils.dump_gms_version(
+ self.discoverer
+ ),
+ '04_target_GMS_version': setup_utils.dump_gms_version(
+ self.advertiser
+ ),
+ '05_test_result': self._test_result_messages,
+ },
+ })
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/bettertogether/betocq/function_tests/bt_ble_function_test_actor.py b/tests/bettertogether/betocq/function_tests/bt_ble_function_test_actor.py
new file mode 100644
index 000000000..92b89e262
--- /dev/null
+++ b/tests/bettertogether/betocq/function_tests/bt_ble_function_test_actor.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2024 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.
+
+"""Bluetooth nearby connection functional test actor."""
+
+from betocq import nc_constants
+from betocq import nearby_connection_wrapper
+from betocq.function_tests import function_test_actor_base
+
+
+class BtBleFunctionTestActor(function_test_actor_base.FunctionTestActorBase):
+ """The actor for running the BT/BlE function test."""
+
+ def test_bt_ble_connection(self):
+ """Test the basic BT and BLE connection.
+
+ Use BLE as discovery/advertising medium
+ Use BT as connection and upgrade medium
+ """
+
+ # 1. set up BT connection
+ advertising_discovery_medium = nc_constants.NearbyMedium.BLE_ONLY
+
+ nearby_snippet = nearby_connection_wrapper.NearbyConnectionWrapper(
+ self.advertiser,
+ self.discoverer,
+ self.advertiser.nearby,
+ self.discoverer.nearby,
+ advertising_discovery_medium=advertising_discovery_medium,
+ connection_medium=nc_constants.NearbyMedium.BT_ONLY,
+ upgrade_medium=nc_constants.NearbyMedium.BT_ONLY,
+ )
+ connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts(
+ nc_constants.FIRST_DISCOVERY_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_INIT_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_RESULT_TIMEOUT)
+ try:
+ nearby_snippet.start_nearby_connection(
+ timeouts=connection_setup_timeouts,
+ medium_upgrade_type=nc_constants.MediumUpgradeType.NON_DISRUPTIVE)
+ finally:
+ self._test_failure_reason = nearby_snippet.test_failure_reason
+ self._test_result.file_transfer_nc_setup_quality_info = (
+ nearby_snippet.connection_quality_info
+ )
+
+ # 2. transfer file through bluetooth
+ try:
+ self._test_result.file_transfer_throughput_kbps = (
+ nearby_snippet.transfer_file(
+ nc_constants.TRANSFER_FILE_SIZE_1KB,
+ nc_constants.BT_1K_PAYLOAD_TRANSFER_TIMEOUT,
+ nc_constants.PayloadType.FILE,
+ )
+ )
+ finally:
+ self._test_failure_reason = nearby_snippet.test_failure_reason
+
+ # 3. disconnect
+ nearby_snippet.disconnect_endpoint()
+
+ def get_test_result_message(self) -> str:
+ """Gets the test result of current test."""
+ if (
+ self._test_failure_reason
+ == nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ return 'PASS'
+ if (
+ self._test_failure_reason
+ is nc_constants.SingleTestFailureReason.FILE_TRANSFER_FAIL
+ ):
+ return 'The Bluetooth performance is really bad or unknown reason.'
+ return ''.join([
+ f'FAIL: due to {self._test_failure_reason.name} - ',
+ f'{nc_constants.COMMON_TRIAGE_TIP.get(self._test_failure_reason)}'
+ ])
diff --git a/tests/bettertogether/betocq/function_tests/bt_multiplex_function_test_actor.py b/tests/bettertogether/betocq/function_tests/bt_multiplex_function_test_actor.py
new file mode 100644
index 000000000..2f5980416
--- /dev/null
+++ b/tests/bettertogether/betocq/function_tests/bt_multiplex_function_test_actor.py
@@ -0,0 +1,133 @@
+# Copyright (C) 2024 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.
+
+"""Bluetooth nearby connection functional test actor."""
+
+from mobly import asserts
+
+from betocq import nc_constants
+from betocq import nearby_connection_wrapper
+from betocq.function_tests import function_test_actor_base
+
+
+class BtMultiplexFunctionTestActor(
+ function_test_actor_base.FunctionTestActorBase):
+ """The actor for running BT/BlE multiplex function test.
+
+ It allows to setup multiple BT connections.
+ """
+
+ def test_bt_multiplex_connections(self):
+ """Test the capability of setting up two BT connections (multiplex).
+
+ This is only required for specific CUJ: quick start
+ step 1: set up 2 BT connections
+ step 2: transfer a small file with the 2nd BT connection
+ """
+ if (
+ self.test_parameters.target_cuj_name
+ != nc_constants.TARGET_CUJ_QUICK_START
+ and not self.test_parameters.requires_bt_multiplex
+ ):
+ self._skipped = True
+ asserts.skip(
+ 'BT multiplex is not required for this CUJ -'
+ f' {self.test_parameters.target_cuj_name}'
+ )
+
+ # 1. set up 1st BT connection
+ advertising_discovery_medium = nc_constants.NearbyMedium.BLE_ONLY
+ nearby_snippet_2 = nearby_connection_wrapper.NearbyConnectionWrapper(
+ self.advertiser,
+ self.discoverer,
+ self.advertiser.nearby2,
+ self.discoverer.nearby2,
+ advertising_discovery_medium=advertising_discovery_medium,
+ connection_medium=nc_constants.NearbyMedium.BT_ONLY,
+ upgrade_medium=nc_constants.NearbyMedium.BT_ONLY,
+ )
+ connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts(
+ nc_constants.FIRST_DISCOVERY_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_INIT_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_RESULT_TIMEOUT)
+
+ try:
+ nearby_snippet_2.start_nearby_connection(
+ timeouts=connection_setup_timeouts,
+ medium_upgrade_type=nc_constants.MediumUpgradeType.NON_DISRUPTIVE)
+ finally:
+ self._test_failure_reason = nearby_snippet_2.test_failure_reason
+ self._test_result.prior_nc_setup_quality_info = (
+ nearby_snippet_2.connection_quality_info
+ )
+ # 2nd bt connection
+ nearby_snippet = nearby_connection_wrapper.NearbyConnectionWrapper(
+ self.advertiser,
+ self.discoverer,
+ self.advertiser.nearby,
+ self.discoverer.nearby,
+ advertising_discovery_medium=advertising_discovery_medium,
+ connection_medium=nc_constants.NearbyMedium.BT_ONLY,
+ upgrade_medium=nc_constants.NearbyMedium.BT_ONLY,
+ )
+
+ second_connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts(
+ nc_constants.SECOND_DISCOVERY_TIMEOUT,
+ nc_constants.SECOND_CONNECTION_INIT_TIMEOUT,
+ nc_constants.SECOND_CONNECTION_RESULT_TIMEOUT)
+ try:
+ nearby_snippet.start_nearby_connection(
+ timeouts=second_connection_setup_timeouts,
+ medium_upgrade_type=nc_constants.MediumUpgradeType.NON_DISRUPTIVE)
+ finally:
+ self._test_failure_reason = nearby_snippet.test_failure_reason
+ self._test_result.file_transfer_nc_setup_quality_info = (
+ nearby_snippet.connection_quality_info
+ )
+
+ # 2. transfer file through bluetooth
+ try:
+ self._test_result.file_transfer_throughput_kbps = (
+ nearby_snippet.transfer_file(
+ nc_constants.TRANSFER_FILE_SIZE_1KB,
+ nc_constants.BT_1K_PAYLOAD_TRANSFER_TIMEOUT,
+ nc_constants.PayloadType.FILE))
+ finally:
+ self._test_failure_reason = nearby_snippet.test_failure_reason
+
+ # 3. disconnect
+ nearby_snippet_2.disconnect_endpoint()
+ nearby_snippet.disconnect_endpoint()
+
+ def get_test_result_message(self) -> str:
+ if self._skipped:
+ return (
+ 'SKIPPED - not required for the target CUJ: '
+ f'{self.test_parameters.target_cuj_name}'
+ )
+ if (
+ self._test_failure_reason
+ == nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ return 'PASS'
+ if (
+ self._test_failure_reason
+ is nc_constants.SingleTestFailureReason.FILE_TRANSFER_FAIL
+ ):
+ return 'The Bluetooth performance is really bad.'
+ else:
+ return ''.join([
+ f'FAIL: due to {self._test_failure_reason.name} - ',
+ f'{nc_constants.COMMON_TRIAGE_TIP.get(self._test_failure_reason)}'
+ ])
diff --git a/tests/bettertogether/betocq/function_tests/fixed_wifi_medium_function_test_actor.py b/tests/bettertogether/betocq/function_tests/fixed_wifi_medium_function_test_actor.py
new file mode 100644
index 000000000..2f035399f
--- /dev/null
+++ b/tests/bettertogether/betocq/function_tests/fixed_wifi_medium_function_test_actor.py
@@ -0,0 +1,150 @@
+# Copyright (C) 2024 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.
+
+"""Base class for all fixed wifi medium function test actors."""
+import logging
+from mobly import asserts
+
+from betocq import nc_constants
+from betocq import nearby_connection_wrapper
+from betocq import setup_utils
+from betocq.function_tests import function_test_actor_base
+
+
+class FixedWifiMediumFunctionTestActor(
+ function_test_actor_base.FunctionTestActorBase):
+ """The base of actors for running the specified fixed wifi D2D medium."""
+
+ def run_fixed_wifi_medium_test(
+ self, wifi_medium: nc_constants.NearbyMedium) -> None:
+ """upgrade the medium from BT to the specified medium and transfer a sample data."""
+ self._wifi_medium_under_test = wifi_medium
+ self._test_result = nc_constants.SingleTestResult()
+
+ # 1. set up BT and WiFi connection
+ advertising_discovery_medium = nc_constants.NearbyMedium(
+ self.test_parameters.advertising_discovery_medium
+ )
+ nearby_snippet = nearby_connection_wrapper.NearbyConnectionWrapper(
+ self.advertiser,
+ self.discoverer,
+ self.advertiser.nearby,
+ self.discoverer.nearby,
+ advertising_discovery_medium=advertising_discovery_medium,
+ connection_medium=nc_constants.NearbyMedium.BT_ONLY,
+ upgrade_medium=self._wifi_medium_under_test,
+ )
+
+ connection_setup_timeouts = nc_constants.ConnectionSetupTimeouts(
+ nc_constants.FIRST_DISCOVERY_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_INIT_TIMEOUT,
+ nc_constants.FIRST_CONNECTION_RESULT_TIMEOUT)
+
+ try:
+ nearby_snippet.start_nearby_connection(
+ timeouts=connection_setup_timeouts,
+ medium_upgrade_type=nc_constants.MediumUpgradeType.NON_DISRUPTIVE)
+ finally:
+ self._test_failure_reason = nearby_snippet.test_failure_reason
+ self._test_result.file_transfer_nc_setup_quality_info = (
+ nearby_snippet.connection_quality_info
+ )
+
+ # 2. transfer file through WiFi
+ try:
+ self._test_result.file_transfer_throughput_kbps = (
+ nearby_snippet.transfer_file(
+ nc_constants.TRANSFER_FILE_SIZE_1KB,
+ nc_constants.WIFI_1K_PAYLOAD_TRANSFER_TIMEOUT,
+ nc_constants.PayloadType.FILE))
+ finally:
+ self._test_failure_reason = nearby_snippet.test_failure_reason
+
+ # 3. disconnect
+ nearby_snippet.disconnect_endpoint()
+
+ def connect_to_wifi(self):
+ if self.test_parameters.toggle_airplane_mode_target_side:
+ setup_utils.toggle_airplane_mode(self.advertiser)
+
+ wifi_ssid, wifi_password = self._get_wifi_ssid_password()
+
+ if not wifi_ssid:
+ self._test_failure_reason = (
+ nc_constants.SingleTestFailureReason.AP_IS_NOT_CONFIGURED
+ )
+ asserts.fail('Wifi AP must be specified.')
+
+ logging.info('connect to wifi: %s', wifi_ssid)
+
+ # source device
+ self._test_failure_reason = (
+ nc_constants.SingleTestFailureReason.SOURCE_WIFI_CONNECTION
+ )
+ discoverer_wifi_latency = setup_utils.connect_to_wifi_sta_till_success(
+ self.discoverer, wifi_ssid, wifi_password
+ )
+ self.discoverer.log.info(
+ 'connecting to wifi in '
+ f'{round(discoverer_wifi_latency.total_seconds())} s'
+ )
+ # target device
+ self._test_failure_reason = (
+ nc_constants.SingleTestFailureReason.TARGET_WIFI_CONNECTION
+ )
+ advertiser_wlan_latency = setup_utils.connect_to_wifi_sta_till_success(
+ self.advertiser, wifi_ssid, wifi_password)
+ self.advertiser.log.info(
+ 'connecting to wifi in '
+ f'{round(advertiser_wlan_latency.total_seconds())} s')
+ self.advertiser.log.info(
+ self.advertiser.nearby.wifiGetConnectionInfo().get('mFrequency')
+ )
+ self._test_failure_reason = (
+ nc_constants.SingleTestFailureReason.SUCCESS
+ )
+
+ def get_test_result_message(self) -> str:
+ if (
+ self._test_failure_reason
+ == nc_constants.SingleTestFailureReason.SUCCESS
+ ):
+ return 'PASS'
+ if (
+ self._test_failure_reason
+ == nc_constants.SingleTestFailureReason.WIFI_MEDIUM_UPGRADE
+ ):
+ return f'{self._test_failure_reason.name} - '.join(
+ self._get_medium_upgrade_failure_tip()
+ )
+ if (
+ self._test_failure_reason
+ == nc_constants.SingleTestFailureReason.FILE_TRANSFER_FAIL
+ ):
+ return f'{self._test_failure_reason.name} - '.join(
+ self._get_file_transfer_failure_tip()
+ )
+ return ''.join([
+ f'{self._test_failure_reason.name} - ',
+ nc_constants.COMMON_TRIAGE_TIP.get(self._test_failure_reason),
+ ])
+
+ def _get_medium_upgrade_failure_tip(self) -> str:
+ return nc_constants.MEDIUM_UPGRADE_FAIL_TRIAGE_TIPS.get(
+ self._wifi_medium_under_test, 'unsupported test medium')
+
+ def _get_file_transfer_failure_tip(self) -> str:
+ if self._wifi_medium_under_test is not None:
+ return f'{self._wifi_medium_under_test.name}: the performance is too bad.'
+ asserts.fail('unexpected calling of _get_file_transfer_failure_tip')
diff --git a/tests/bettertogether/betocq/function_tests/function_test_actor_base.py b/tests/bettertogether/betocq/function_tests/function_test_actor_base.py
new file mode 100644
index 000000000..c0ecf122d
--- /dev/null
+++ b/tests/bettertogether/betocq/function_tests/function_test_actor_base.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2024 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.
+
+"""Provides the actor base for all function tests."""
+
+from typing import Tuple
+
+from mobly.controllers import android_device
+
+from betocq import nc_constants
+
+
+class FunctionTestActorBase:
+ """Base class of actors for running all function tests."""
+
+ def __init__(self,
+ test_parameters: nc_constants.TestParameters,
+ discoverer: android_device.AndroidDevice,
+ advertiser: android_device.AndroidDevice
+ ):
+ self.test_parameters: nc_constants.TestParameters = test_parameters
+ self.advertiser: android_device.AndroidDevice = advertiser
+ self.discoverer: android_device.AndroidDevice = discoverer
+ self._test_result: nc_constants.SingleTestResult = (
+ nc_constants.SingleTestResult()
+ )
+ self._test_failure_reason: nc_constants.SingleTestFailureReason = (
+ nc_constants.SingleTestFailureReason.UNINITIALIZED
+ )
+ self._wifi_medium_under_test = None
+ self._skipped: bool = False
+
+ def _get_wifi_ssid_password(self) -> Tuple[str, str]:
+ """Returns the available wifi username and password."""
+ if self.test_parameters.wifi_ssid:
+ return (
+ self.test_parameters.wifi_ssid,
+ self.test_parameters.wifi_password,
+ )
+ if self.test_parameters.wifi_5g_ssid:
+ return (
+ self.test_parameters.wifi_5g_ssid,
+ self.test_parameters.wifi_5g_password,
+ )
+ if self.test_parameters.wifi_dfs_5g_ssid:
+ return (
+ self.test_parameters.wifi_dfs_5g_ssid,
+ self.test_parameters.wifi_dfs_5g_password,
+ )
+ if self.test_parameters.wifi_2g_ssid:
+ return (
+ self.test_parameters.wifi_2g_ssid,
+ self.test_parameters.wifi_2g_password,
+ )
+ return ('', '')
+
+ def get_test_result_message(self) -> str:
+ """Returns the message about the test result."""
+ return 'Unknown'
+
+ def _get_test_failure_reason(self) -> nc_constants.SingleTestFailureReason:
+ """Returns the test failure reason."""
+ return self._test_failure_reason
diff --git a/tests/bettertogether/betocq/function_tests/local_dev_function_group_testbed.yml b/tests/bettertogether/betocq/function_tests/local_dev_function_group_testbed.yml
new file mode 100644
index 000000000..ce7eac557
--- /dev/null
+++ b/tests/bettertogether/betocq/function_tests/local_dev_function_group_testbed.yml
@@ -0,0 +1,26 @@
+# Test bed for the function tests only.
+x-test-params: &test-params
+ wifi_ssid: "anyAp"
+ wifi_password: "anyAp"
+ target_cuj_name: "quick_start"
+
+x-controllers: &controllers
+ Controllers:
+ AndroidDevice:
+ - serial: "123456ABCDEF"
+ role: "source_device"
+ - serial: "ABCDEF123456"
+ role: "target_device"
+
+TestBeds:
+- Name: LocalDevTestbed
+ # only for development, DO NOT port to AOSP.
+ Controllers:
+ AndroidDevice: '*'
+ TestParams:
+ <<: *test-params
+
+- Name: LocalFunctionGroupTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params \ No newline at end of file
diff --git a/tests/bettertogether/betocq/gms_auto_updates_util.py b/tests/bettertogether/betocq/gms_auto_updates_util.py
new file mode 100644
index 000000000..f8dab4faf
--- /dev/null
+++ b/tests/bettertogether/betocq/gms_auto_updates_util.py
@@ -0,0 +1,176 @@
+# Copyright (C) 2024 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.
+
+"""class to enable/disable GMS auto update."""
+
+import logging
+import os
+import tempfile
+from xml import etree
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import adb
+
+
+_FINSKY_CONFIG_FILE = '/data/data/com.android.vending/shared_prefs/finsky.xml'
+_FINSKY_CONFIG_NAME = 'auto_update_enabled'
+_FINSKY_CONFIG_VALUE_DISABLE = 'false'
+_FINSKY_CONFIG_VALUE_ENABLE = 'true'
+_VENDING_CONFIG_FILE = '/data/data/com.android.vending/shared_prefs/com.android.vending_preferences.xml'
+_VENDING_CONFIG_NAME = 'auto-update-mode'
+_VENDING_CONFIG_VALUE_DISABLE = 'AUTO_UPDATE_NEVER'
+_VENDING_CONFIG_VALUE_ENABLE = 'AUTO_UPDATE_WIFI'
+_BLANK_CONFIG = '<?xml version="1.0" encoding="utf-8"?><map></map>'
+_XML_BOOL_TYPE = 'boolean'
+_XML_STRING_TYPE = 'string'
+_ENABLE_GSERVICES_CMD_TEMPLATE = [
+ (
+ 'am broadcast '
+ '-a com.google.gservices.intent.action.GSERVICES_OVERRIDE '
+ '-e finsky.play_services_auto_update_enabled {}'
+ ),
+ (
+ 'am broadcast '
+ '-a com.google.gservices.intent.action.GSERVICES_OVERRIDE '
+ '-e finsky.setup_wizard_additional_account_vpa_enable {}'
+ ),
+]
+
+
+class GmsAutoUpdatesUtil:
+ """class to enable/disable GMS auto updates."""
+
+ def __init__(self, ad: android_device.AndroidDevice):
+ self._device: android_device.AndroidDevice = ad
+
+ def enable_gms_auto_updates(self) -> None:
+ self._config_gms_auto_updates(True)
+
+ def disable_gms_auto_updates(self) -> None:
+ self._config_gms_auto_updates(False)
+
+ def _config_gms_auto_updates(self, enable_updates: bool) -> None:
+ """Configures GMS auto updates."""
+ if not self._device.is_adb_root:
+ self._device.log.info(
+ f'failed to set the play store auto updates as {enable_updates}'
+ 'you should enable/disable it manually on an unrooted device.')
+ else:
+ if enable_updates:
+ self._configure_play_store_updates(
+ _FINSKY_CONFIG_VALUE_ENABLE, _VENDING_CONFIG_VALUE_ENABLE
+ )
+ else:
+ self._configure_play_store_updates(
+ _FINSKY_CONFIG_VALUE_DISABLE, _VENDING_CONFIG_VALUE_DISABLE
+ )
+ self._configure_gservice_updates(enable_updates)
+
+ def _configure_gservice_updates(self, enable_updates: bool) -> None:
+ """Overwites Gservice to enable/disable updates."""
+ for cmd in _ENABLE_GSERVICES_CMD_TEMPLATE:
+ self._device.adb.shell(
+ cmd.format('true' if enable_updates else 'false')
+ )
+
+ def _create_or_update_play_store_config(
+ self,
+ tmp_dir: str,
+ value_type: str,
+ name: str,
+ value: str,
+ device_path: str,
+ ) -> str:
+ """Creates or updates a Play Store configuration file.
+
+ The function retrieves the Play Store configuration file from the device
+ then update it. If the file does not exist, it creates a new one.
+
+ Args:
+ tmp_dir: The temporary directory to store the configuration file.
+ value_type: The type of the configuration field.
+ name: The name of the configuration field.
+ value: The value of the configuration field.
+ device_path: The path to the configuration file on the device.
+
+ Returns:
+ The path to the updated configuration file.
+ """
+ path = os.path.join(tmp_dir, f'play_store_config_{name}.xml')
+ try:
+ self._device.adb.pull([device_path, path])
+ except adb.AdbError as e:
+ self._device.log.warning('failed to pull %s: %s', device_path, e)
+
+ config_doc = etree.ElementTree.parse(path) if os.path.isfile(path) else None
+
+ changing_element = None
+ root = (
+ etree.ElementTree.fromstring(_BLANK_CONFIG.encode())
+ if config_doc is None
+ else config_doc.getroot()
+ )
+
+ # find the element, xPath doesn't work as the name is a reserved word.
+ for child in root:
+ if child.attrib['name'] == name:
+ changing_element = child
+ break
+ if changing_element is None:
+ if value_type == _XML_BOOL_TYPE:
+ changing_element = etree.ElementTree.SubElement(root, 'boolean')
+ else:
+ changing_element = etree.ElementTree.SubElement(root, 'string')
+ logging.info('element for %s is %s, %s', name, changing_element.tag,
+ changing_element.attrib)
+ if value_type == _XML_BOOL_TYPE:
+ changing_element.set('name', name)
+ changing_element.set('value', value)
+ else:
+ changing_element.attrib['name'] = name
+ changing_element.text = value
+
+ tree = etree.ElementTree.ElementTree(root)
+ tree.write(path, xml_declaration=True, encoding='utf-8')
+ return path
+
+ def _configure_play_store_updates(
+ self, finsky_config_value: str, vending_config_value: str
+ ) -> None:
+ """Configures the Play Store update related settings."""
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ finsky_config = self._create_or_update_play_store_config(
+ tmp_dir,
+ _XML_BOOL_TYPE,
+ _FINSKY_CONFIG_NAME,
+ finsky_config_value,
+ _FINSKY_CONFIG_FILE,
+ )
+ self._device.adb.push([finsky_config, _FINSKY_CONFIG_FILE])
+ try:
+ os.remove(finsky_config)
+ except OSError as e:
+ logging.warning('failed to remove %s: %s', finsky_config, e)
+
+ vending_config = self._create_or_update_play_store_config(
+ tmp_dir,
+ _XML_STRING_TYPE,
+ _VENDING_CONFIG_NAME,
+ vending_config_value,
+ _VENDING_CONFIG_FILE,
+ )
+ self._device.adb.push([vending_config, _VENDING_CONFIG_FILE])
+ try:
+ os.remove(vending_config)
+ except OSError as e:
+ logging.warning('failed to remove %s: %s', vending_config, e)
diff --git a/tests/bettertogether/betocq/local_dev_testbed.yml b/tests/bettertogether/betocq/local_dev_testbed.yml
new file mode 100644
index 000000000..e5b21581a
--- /dev/null
+++ b/tests/bettertogether/betocq/local_dev_testbed.yml
@@ -0,0 +1,102 @@
+x-test-params: &test-params
+ allow_unrooted_device: False
+ # for 2G wifi SSID - channel 6, frequency 2437, comment out if it is not available.
+ wifi_2g_ssid: "AP2437"
+ wifi_2g_password: "AP2437"
+ # for 5G wifi SSID - channel 36, frequency 5180, comment out if it is not available.
+ wifi_5g_ssid: "AP5180"
+ wifi_5g_password: "AP5180"
+ # for DFS 5G wifi SSID - channel 52, frequency 5260, comment out if it is not available.
+ wifi_dfs_5g_ssid: "AP5260"
+ wifi_dfs_5g_password: "AP5260"
+ # use the AP controlled by programming dynamically
+ use_auto_controlled_wifi_ap: False
+ run_function_tests_with_performance_tests: True
+
+x-controllers: &controllers
+ Controllers:
+ AndroidDevice:
+ - serial: "123456ABCDEF"
+ role: "source_device"
+ # The max number of spatial streams
+ max_num_streams: 2
+ # The max PHY rate at 5G, in Mbps
+ max_phy_rate_5g_mbps: 2402
+ # The max PHY rate at 2G, in Mbps
+ max_phy_rate_2g_mbps: 287
+ # if the device supports 5G Wifi
+ supports_5g: True
+ # if the device supports DBS (Dual Band Simultaneous) in STA + WiFi-Direct concurrency mode
+ supports_dbs_sta_wfd: True
+ # The max number of spatial streams in DBS mode.
+ max_num_streams_dbs: 2
+ # if the device supports to start WFD group owner at a STA-associated DFS channel
+ enable_sta_dfs_channel_for_peer_network: False
+ # if the device supports to start WFD group owner at a STA-associated indoor channel
+ enable_sta_indoor_channel_for_peer_network: False
+ # 14 - U, 13 - T, 12 - S
+ android_version: 14
+ - serial: "ABCDEF123456"
+ role: "target_device"
+ # The max number of spatial streams
+ max_num_streams: 2
+ # The max PHY rate at 5G, in Mbps
+ max_phy_rate_5g_mbps: 2402
+ # The max PHY rate at 2G, in Mbps
+ max_phy_rate_2g_mbps: 287
+ # if the device supports 5G Wifi
+ supports_5g: True # True
+ # if the device supports DBS (Dual Band Simultaneous) in STA + WiFi-Direct concurrency mode
+ supports_dbs_sta_wfd: True
+ # The max number of spatial streams in DBS mode.
+ max_num_streams_dbs: 2
+ # if the device supports to start WFD group owner at a STA-associated DFS channel
+ enable_sta_dfs_channel_for_peer_network: True
+ # if the device supports to start WFD group owner at a STA-associated indoor channel
+ enable_sta_indoor_channel_for_peer_network: True
+ # 14 - U, 13 - T, 12 - S
+ android_version: 14
+
+TestBeds:
+- Name: LocalPerformanceDefaultTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+
+- Name: LocalQuickstartPerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_start"
+ # before the performance test, run the function tests first
+ run_function_tests_with_performance_tests: True
+ # if the function tests is failed, abort the performance test
+ abort_all_tests_on_function_tests_fail: True
+
+- Name: LocalQuicksharePerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_share"
+
+- Name: LocalEsimTransferPerformanceTestbed
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "setting_based_esim_transfer"
+
+# used only for function test
+- Name: LocalQuickstartFunctionTestbed
+ Controllers:
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_start"
+
+# used only for function test
+- Name: LocalQuickshareFunctionTestbed
+ Controllers:
+ <<: *controllers
+ TestParams:
+ <<: *test-params
+ target_cuj_name: "quick_share" \ No newline at end of file
diff --git a/tests/bettertogether/betocq/nc_base_test.py b/tests/bettertogether/betocq/nc_base_test.py
new file mode 100644
index 000000000..716f65a34
--- /dev/null
+++ b/tests/bettertogether/betocq/nc_base_test.py
@@ -0,0 +1,271 @@
+# Copyright (C) 2024 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.
+
+"""Mobly base test class for Neaby Connections.
+
+Override the NCBaseTestClass#_get_country_code method if the test requires
+a special country code, the 'US' is used by default.
+"""
+
+import logging
+import os
+import time
+
+from mobly import asserts
+from mobly import base_test
+from mobly import records
+from mobly import utils
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import errors
+import yaml
+
+from betocq import android_wifi_utils
+from betocq import nc_constants
+from betocq import setup_utils
+
+NEARBY_SNIPPET_PACKAGE_NAME = 'com.google.android.nearby.mobly.snippet'
+NEARBY_SNIPPET_2_PACKAGE_NAME = 'com.google.android.nearby.mobly.snippet.second'
+
+_CONFIG_EXTERNAL_PATH = 'TBD'
+
+
+class NCBaseTestClass(base_test.BaseTestClass):
+ """The Base of Nearby Connection E2E tests."""
+
+ def __init__(self, configs):
+ super().__init__(configs)
+ self.ads: list[android_device.AndroidDevice] = []
+ self.advertiser: android_device.AndroidDevice = None
+ self.discoverer: android_device.AndroidDevice = None
+ self.test_parameters: nc_constants.TestParameters = (
+ nc_constants.TestParameters.from_user_params(self.user_params)
+ )
+ self._nearby_snippet_apk_path: str = None
+ self._nearby_snippet_2_apk_path: str = None
+ self.performance_test_iterations: int = 1
+ self.num_bug_reports: int = 0
+ self._requires_2_snippet_apks = False
+ self.__loaded_2_nearby_snippets = False
+ self.__skipped_test_class = False
+
+ def _get_skipped_test_class_reason(self) -> str | None:
+ return None
+
+ def setup_class(self) -> None:
+ self._setup_openwrt_wifi()
+ self.ads = self.register_controller(android_device, min_number=2)
+ try:
+ self.discoverer = android_device.get_device(
+ self.ads, role='source_device'
+ )
+ self.advertiser = android_device.get_device(
+ self.ads, role='target_device'
+ )
+ except errors.Error:
+ logging.warning(
+ 'The source,target devices are not specified in testbed;'
+ 'The result may not be expected.'
+ )
+ self.advertiser, self.discoverer = self.ads
+
+ utils.concurrent_exec(
+ self._setup_android_hw_capability,
+ param_list=[[ad] for ad in self.ads],
+ raise_on_exception=True,
+ )
+
+ skipped_test_class_reason = self._get_skipped_test_class_reason()
+ if skipped_test_class_reason:
+ self.__skipped_test_class = True
+ asserts.abort_class(skipped_test_class_reason)
+
+ file_tag = 'files' if 'files' in self.user_params else 'mh_files'
+ self._nearby_snippet_apk_path = self.user_params.get(file_tag, {}).get(
+ 'nearby_snippet', ['']
+ )[0]
+ if self.test_parameters.requires_bt_multiplex:
+ self._requires_2_snippet_apks = True
+ self._nearby_snippet_2_apk_path = self.user_params.get(file_tag, {}).get(
+ 'nearby_snippet_2', ['']
+ )[0]
+
+ # disconnect from all wifi automatically
+ utils.concurrent_exec(
+ android_wifi_utils.forget_all_wifi,
+ param_list=[[ad] for ad in self.ads],
+ raise_on_exception=True,
+ )
+
+ utils.concurrent_exec(
+ self._setup_android_device,
+ param_list=[[ad] for ad in self.ads],
+ raise_on_exception=True,
+ )
+
+ def _setup_openwrt_wifi(self):
+ """Sets up the wifi connection with OpenWRT."""
+ if not self.user_params.get('use_auto_controlled_wifi_ap', False):
+ return
+
+ # TODO(xianyuanjia): Add OpenWRT setup logic here
+ return
+
+ def _setup_android_hw_capability(
+ self, ad: android_device.AndroidDevice
+ ) -> None:
+ ad.android_version = int(ad.adb.getprop('ro.build.version.release'))
+
+ if not os.path.isfile(_CONFIG_EXTERNAL_PATH):
+ return
+
+ config_path = _CONFIG_EXTERNAL_PATH
+ with open(config_path, 'r') as f:
+ rule = yaml.safe_load(f).get(ad.model, None)
+ asserts.assert_is_not_none(
+ rule, f'{ad} Model {ad.model} is not supported in config file'
+ )
+ for key, value in rule.items():
+ setattr(ad, key, value)
+
+ def _get_country_code(self) -> str:
+ return 'US'
+
+ def _setup_android_device(self, ad: android_device.AndroidDevice) -> None:
+ ad.debug_tag = ad.serial + '(' + ad.adb.getprop('ro.product.model') + ')'
+ if not ad.is_adb_root:
+ if self.test_parameters.allow_unrooted_device:
+ ad.log.info('Unrooted device is detected. Test coverage is limited')
+ else:
+ asserts.abort_all('The test only can run on rooted device.')
+
+ setup_utils.disable_gms_auto_updates(ad)
+
+ ad.debug_tag = ad.serial + '(' + ad.adb.getprop('ro.product.model') + ')'
+ ad.log.info('try to install nearby_snippet_apk')
+ if self._nearby_snippet_apk_path:
+ setup_utils.install_apk(ad, self._nearby_snippet_apk_path)
+ else:
+ ad.log.warning(
+ 'nearby_snippet apk is not specified, '
+ 'make sure it is installed in the device'
+ )
+
+ ad.log.info('grant manage external storage permission')
+ setup_utils.grant_manage_external_storage_permission(
+ ad, NEARBY_SNIPPET_PACKAGE_NAME
+ )
+ ad.load_snippet('nearby', NEARBY_SNIPPET_PACKAGE_NAME)
+
+ if self._requires_2_snippet_apks:
+ ad.log.info('try to install nearby_snippet_2_apk')
+ if self._nearby_snippet_2_apk_path:
+ setup_utils.install_apk(ad, self._nearby_snippet_2_apk_path)
+ else:
+ ad.log.warning(
+ 'nearby_snippet_2 apk is not specified, '
+ 'make sure it is installed in the device'
+ )
+ setup_utils.grant_manage_external_storage_permission(
+ ad, NEARBY_SNIPPET_2_PACKAGE_NAME
+ )
+ setup_utils.enable_bluetooth_multiplex(ad)
+ ad.load_snippet('nearby2', NEARBY_SNIPPET_2_PACKAGE_NAME)
+ self.__loaded_2_nearby_snippets = True
+ if not ad.nearby.wifiIsEnabled():
+ ad.nearby.wifiEnable()
+ setup_utils.disconnect_from_wifi(ad)
+ setup_utils.enable_logs(ad)
+
+ setup_utils.disable_redaction(ad)
+
+ setup_utils.set_country_code(ad, self._get_country_code())
+
+ def setup_test(self):
+ self.record_data({
+ 'Test Name': self.current_test_info.name,
+ 'sponge_properties': {
+ 'beto_team': 'Nearby Connections',
+ 'beto_feature': 'Nearby Connections',
+ },
+ })
+ self._reset_nearby_connection()
+
+ def _reset_wifi_connection(self) -> None:
+ """Resets wifi connections on both devices."""
+ self.discoverer.nearby.wifiClearConfiguredNetworks()
+ self.advertiser.nearby.wifiClearConfiguredNetworks()
+ time.sleep(nc_constants.WIFI_DISCONNECTION_DELAY.total_seconds())
+
+ def _reset_nearby_connection(self) -> None:
+ """Resets nearby connection."""
+ self.discoverer.nearby.stopDiscovery()
+ self.discoverer.nearby.stopAllEndpoints()
+ self.advertiser.nearby.stopAdvertising()
+ self.advertiser.nearby.stopAllEndpoints()
+ if self.__loaded_2_nearby_snippets:
+ self.discoverer.nearby2.stopDiscovery()
+ self.discoverer.nearby2.stopAllEndpoints()
+ self.advertiser.nearby2.stopAdvertising()
+ self.advertiser.nearby2.stopAllEndpoints()
+ time.sleep(nc_constants.NEARBY_RESET_WAIT_TIME.total_seconds())
+
+ def _teardown_device(self, ad: android_device.AndroidDevice) -> None:
+ ad.nearby.transferFilesCleanup()
+ setup_utils.enable_gms_auto_updates(ad)
+
+ if self.test_parameters.disconnect_wifi_after_test:
+ setup_utils.disconnect_from_wifi(ad)
+
+ ad.unload_snippet('nearby')
+ if self.__loaded_2_nearby_snippets:
+ ad.unload_snippet('nearby2')
+
+ def teardown_test(self) -> None:
+ utils.concurrent_exec(
+ lambda d: d.services.create_output_excerpts_all(self.current_test_info),
+ param_list=[[ad] for ad in self.ads],
+ raise_on_exception=True,
+ )
+
+ def teardown_class(self) -> None:
+ if self.__skipped_test_class:
+ logging.info('Skipping teardown class.')
+ return
+
+ # handle summary results
+ self._summary_test_results()
+
+ utils.concurrent_exec(
+ self._teardown_device,
+ param_list=[[ad] for ad in self.ads],
+ raise_on_exception=True,
+ )
+
+ if hasattr(self, 'openwrt') and hasattr(self, 'wifi_info'):
+ self.openwrt.stop_wifi(self.wifi_info)
+
+ def _summary_test_results(self) -> None:
+ pass
+
+ def on_fail(self, record: records.TestResultRecord) -> None:
+ if self.__skipped_test_class:
+ logging.info('Skipping on_fail.')
+ return
+ self.num_bug_reports = self.num_bug_reports + 1
+ if self.num_bug_reports <= nc_constants.MAX_NUM_BUG_REPORT:
+ logging.info('take bug report for failure')
+ android_device.take_bug_reports(
+ self.ads,
+ destination=self.current_test_info.output_path,
+ )
diff --git a/tests/bettertogether/betocq/nc_constants.py b/tests/bettertogether/betocq/nc_constants.py
new file mode 100644
index 000000000..e3eaba740
--- /dev/null
+++ b/tests/bettertogether/betocq/nc_constants.py
@@ -0,0 +1,366 @@
+# Copyright (C) 2024 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.
+
+"""Constants for Nearby Connection."""
+
+import ast
+import dataclasses
+import datetime
+import enum
+import logging
+from typing import Any
+
+SUCCESS_RATE_TARGET = 0.95 # 95%
+MCC_PERFORMANCE_TEST_COUNT = 100
+MCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 5
+SCC_PERFORMANCE_TEST_COUNT = 10
+SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 2
+BT_PERFORMANCE_TEST_COUNT = 100
+BT_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 5
+
+NEARBY_RESET_WAIT_TIME = datetime.timedelta(seconds=5)
+WIFI_DISCONNECTION_DELAY = datetime.timedelta(seconds=3)
+
+FIRST_DISCOVERY_TIMEOUT = datetime.timedelta(seconds=30)
+FIRST_CONNECTION_INIT_TIMEOUT = datetime.timedelta(seconds=30)
+FIRST_CONNECTION_RESULT_TIMEOUT = datetime.timedelta(seconds=35)
+BT_1K_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=20)
+BT_1M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=50)
+BLE_1M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=50)
+SECOND_DISCOVERY_TIMEOUT = datetime.timedelta(seconds=35)
+SECOND_CONNECTION_INIT_TIMEOUT = datetime.timedelta(seconds=10)
+SECOND_CONNECTION_RESULT_TIMEOUT = datetime.timedelta(seconds=25)
+CONNECTION_BANDWIDTH_CHANGED_TIMEOUT = datetime.timedelta(seconds=25)
+WIFI_1K_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=20)
+WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=20)
+WIFI_200M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=100)
+WIFI_STA_CONNECTING_TIME_OUT = datetime.timedelta(seconds=25)
+DISCONNECTION_TIMEOUT = datetime.timedelta(seconds=15)
+
+MAX_PHY_RATE_PER_STREAM_AC_80_MBPS = 433
+MAX_PHY_RATE_PER_STREAM_AC_40_MBPS = 200
+MAX_PHY_RATE_PER_STREAM_N_20_MBPS = 72
+
+MCC_THROUGHPUT_MULTIPLIER = 0.25
+MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_5G = 0.37
+MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_2G = 0.4
+WIFI_HOTSPOT_THROUGHPUT_MULTIPLIER = 0.7
+
+CLASSIC_BT_MEDIUM_THROUGHPUT_BENCHMARK = 20 # 20KBps
+BLE_MEDIUM_THROUGHPUT_BENCHMARK = 20 # 20KBps
+
+KEEP_ALIVE_TIMEOUT_BT_MS = 30000
+KEEP_ALIVE_INTERVAL_BT_MS = 5000
+
+KEEP_ALIVE_TIMEOUT_WIFI_MS = 10000
+KEEP_ALIVE_INTERVAL_WIFI_MS = 3000
+
+PERCENTILE_50_FACTOR = 0.5
+LATENCY_PRECISION_DIGITS = 1
+
+UNSET_LATENCY = datetime.timedelta.max
+UNSET_THROUGHPUT_KBPS = -1.0
+MAX_NUM_BUG_REPORT = 5
+
+TRANSFER_FILE_SIZE_200MB = 200 * 1024 # kB
+TRANSFER_FILE_SIZE_20MB = 20 * 1024 # kB
+TRANSFER_FILE_SIZE_1MB = 1024 # kB
+TRANSFER_FILE_SIZE_1KB = 1 # kB
+
+TARGET_CUJ_QUICK_START = 'quick_start'
+TARGET_CUJ_ESIM = 'setting_based_esim_transfer'
+TARGET_CUJ_QUICK_SHARE = 'quick_share'
+
+
+@enum.unique
+class PayloadType(enum.IntEnum):
+ FILE = 2
+ STREAM = 3
+
+
+@enum.unique
+class NearbyMedium(enum.IntEnum):
+ """Medium options for discovery, advertising, connection and upgrade."""
+
+ AUTO = 0
+ BT_ONLY = 1
+ BLE_ONLY = 2
+ WIFILAN_ONLY = 3
+ WIFIAWARE_ONLY = 4
+ UPGRADE_TO_WEBRTC = 5
+ UPGRADE_TO_WIFIHOTSPOT = 6
+ UPGRADE_TO_WIFIDIRECT = 7
+ BLE_L2CAP_ONLY = 8
+ # including WIFI_LAN, WIFI_HOTSPOT, WIFI_DIRECT
+ UPGRADE_TO_ALL_WIFI = 9
+
+
+@dataclasses.dataclass(frozen=False)
+class TestParameters:
+ """Test parameters to be customized for Nearby Connection."""
+
+ target_cuj_name: str = 'unspecified'
+ requires_bt_multiplex: bool = False
+ run_function_tests_with_performance_tests: bool = False
+ abort_all_tests_on_function_tests_fail: bool = True
+ fast_fail_on_any_error: bool = False
+ use_auto_controlled_wifi_ap: bool = False
+ wifi_2g_ssid: str = ''
+ wifi_2g_password: str = ''
+ wifi_5g_ssid: str = ''
+ wifi_5g_password: str = ''
+ wifi_dfs_5g_ssid: str = ''
+ wifi_dfs_5g_password: str = ''
+ wifi_ssid: str = '' # optional, for tests which can use any wifi
+ wifi_password: str = ''
+ advertising_discovery_medium: NearbyMedium = NearbyMedium.BLE_ONLY
+ toggle_airplane_mode_target_side: bool = False
+ reset_wifi_connection: bool = True
+ disconnect_bt_after_test: bool = False
+ disconnect_wifi_after_test: bool = False
+ payload_type: PayloadType = PayloadType.FILE
+ allow_unrooted_device: bool = False
+ keep_alive_timeout_ms: int = KEEP_ALIVE_TIMEOUT_WIFI_MS
+ keep_alive_interval_ms: int = KEEP_ALIVE_INTERVAL_WIFI_MS
+
+ @classmethod
+ def from_user_params(
+ cls,
+ user_params: dict[str, Any]) -> 'TestParameters':
+ """convert the parameters from the testbed to the test parameter."""
+
+ # Convert user int parameter in str format to int.
+ for key, value in user_params.items():
+ if key == 'mh_files':
+ continue
+ logging.info('[Test Parameters] %s: %s', key, value)
+ if isinstance(value, str) and value.isdigit():
+ user_params[key] = ast.literal_eval(value)
+
+ test_parameters_names = {
+ field.name for field in dataclasses.fields(cls)
+ }
+ test_parameters = cls(
+ **{
+ key: val
+ for key, val in user_params.items()
+ if key in test_parameters_names
+ }
+ )
+
+ if test_parameters.target_cuj_name == TARGET_CUJ_QUICK_START:
+ test_parameters.requires_bt_multiplex = True
+
+ return test_parameters
+
+
+@enum.unique
+class NearbyConnectionMedium(enum.IntEnum):
+ """The final connection medium selected, see BandWidthInfo.Medium."""
+ UNKNOWN = 0
+ # reserved 1, it's Medium.MDNS, not used now
+ BLUETOOTH = 2
+ WIFI_HOTSPOT = 3
+ BLE = 4
+ WIFI_LAN = 5
+ WIFI_AWARE = 6
+ NFC = 7
+ WIFI_DIRECT = 8
+ WEB_RTC = 9
+ # 10 is reserved.
+ USB = 11
+
+
+def is_high_quality_medium(medium: NearbyMedium) -> bool:
+ return medium in {
+ NearbyMedium.WIFILAN_ONLY,
+ NearbyMedium.WIFIAWARE_ONLY,
+ NearbyMedium.UPGRADE_TO_WEBRTC,
+ NearbyMedium.UPGRADE_TO_WIFIHOTSPOT,
+ NearbyMedium.UPGRADE_TO_WIFIDIRECT,
+ NearbyMedium.UPGRADE_TO_ALL_WIFI,
+ }
+
+
+@enum.unique
+class MediumUpgradeType(enum.IntEnum):
+ DEFAULT = 0
+ DISRUPTIVE = 1
+ NON_DISRUPTIVE = 2
+
+
+@enum.unique
+class WifiD2DType(enum.IntEnum):
+ SCC_2G = 0
+ SCC_5G = 1
+ MCC_2G_WFD_5G_STA = 2
+ MCC_2G_WFD_5G_INDOOR_STA = 3
+ MCC_5G_WFD_5G_DFS_STA = 4
+ MCC_5G_HS_5G_DFS_STA = 5
+
+
+@enum.unique
+class SingleTestFailureReason(enum.IntEnum):
+ """The failure reasons for a nearby connect connection test."""
+ UNINITIALIZED = 0
+ SOURCE_START_DISCOVERY = 1
+ TARGET_START_ADVERTISING = 2
+ SOURCE_REQUEST_CONNECTION = 3
+ TARGET_ACCEPT_CONNECTION = 4
+ WIFI_MEDIUM_UPGRADE = 5
+ FILE_TRANSFER_FAIL = 6
+ FILE_TRANSFER_THROUGHPUT_LOW = 7
+ SOURCE_WIFI_CONNECTION = 8
+ TARGET_WIFI_CONNECTION = 9
+ AP_IS_NOT_CONFIGURED = 10
+ SUCCESS = 11
+
+
+COMMON_TRIAGE_TIP: dict[SingleTestFailureReason, str] = {
+ SingleTestFailureReason.UNINITIALIZED: (
+ 'not executed, the whole test was exited earlier; the devices may be'
+ 'disconnected from the host, abnormal things, such as system crash, '
+ 'mobly snippet was killed; Or something wrong with the script, check'
+ 'the test running log and the corresponding bugreport log.'
+ ),
+ SingleTestFailureReason.SUCCESS: 'success!',
+ SingleTestFailureReason.SOURCE_START_DISCOVERY: (
+ 'The source device can not start BLE scan.'
+ ),
+ SingleTestFailureReason.TARGET_START_ADVERTISING: (
+ 'The target device can not start BLE advertising.'
+ ),
+ SingleTestFailureReason.SOURCE_REQUEST_CONNECTION: (
+ 'The source device can not connect to the target device.'),
+ SingleTestFailureReason.TARGET_ACCEPT_CONNECTION: (
+ 'The target device can not accept the connection.'),
+ SingleTestFailureReason.SOURCE_WIFI_CONNECTION: (
+ 'The source device can not connect to the wifi network. '
+ '1) Check if the wifi ssid or password is correct;'
+ '2) Try to remove any saved wifi network from wifi settings;'
+ '3) Check if other device can connect to the same AP'
+ '4) Check the wifi related log on the source device.'
+ ),
+ SingleTestFailureReason.TARGET_WIFI_CONNECTION: (
+ '1) Check if the wifi ssid or password is correct;'
+ '2) Try to remove any saved wifi network from wifi settings;'
+ '3) Check if other device can connect to the same AP'
+ '4) Check the wifi related log on the target device.'
+ ),
+ SingleTestFailureReason.AP_IS_NOT_CONFIGURED: (
+ 'The test AP is not set correctly in the test configuration file.'
+ ),
+}
+
+
+MEDIUM_UPGRADE_FAIL_TRIAGE_TIPS: dict[NearbyMedium, str] = {
+ NearbyMedium.WIFILAN_ONLY: (
+ ' WLAN, check if AP blocks mDNS traffic'
+ ),
+ NearbyMedium.UPGRADE_TO_WIFIHOTSPOT: (
+ ' HOTSPOT, check if the WFD group owner fails to start on'
+ ' the target side or the legacy STA fails to connect on the source side'
+ ),
+ NearbyMedium.UPGRADE_TO_WIFIDIRECT: (
+ ' WFD, check if the WFD group owner fails to start on the'
+ ' target side or WFD group client fails to connect on the source side'
+ ),
+ NearbyMedium.UPGRADE_TO_ALL_WIFI: (
+ ' all WiFI mediums, check if WFD, WLAN and HOTSPOT mediums are tried'
+ ' and if the failure is on the target or source side'
+ ),
+}
+
+
+@dataclasses.dataclass(frozen=True)
+class ConnectionSetupTimeouts:
+ """The timeouts of the nearby connection setup."""
+ discovery_timeout: datetime.timedelta | None = None
+ connection_init_timeout: datetime.timedelta | None = None
+ connection_result_timeout: datetime.timedelta | None = None
+
+
+@dataclasses.dataclass(frozen=False)
+class ConnectionSetupQualityInfo:
+ """The quality information of the nearby connection setup."""
+ discovery_latency: datetime.timedelta = UNSET_LATENCY
+ connection_latency: datetime.timedelta = UNSET_LATENCY
+ medium_upgrade_latency: datetime.timedelta = UNSET_LATENCY
+ medium_upgrade_expected: bool = False
+ upgrade_medium: NearbyConnectionMedium | None = None
+
+ def get_dict(self) -> dict[str, str]:
+ dict_repr = {
+ 'discovery': f'{round(self.discovery_latency.total_seconds(), 1)}s',
+ 'connection': f'{round(self.connection_latency.total_seconds(), 1)}s'
+ }
+ if self.medium_upgrade_expected:
+ dict_repr['upgrade'] = (
+ f'{round(self.medium_upgrade_latency.total_seconds(), 1)}s'
+ )
+ if self.upgrade_medium:
+ dict_repr['medium'] = self.upgrade_medium.name
+ return dict_repr
+
+
+@dataclasses.dataclass(frozen=False)
+class SingleTestResult:
+ """The test result of a single iteration."""
+
+ test_iteration: int = 0
+ is_failed_with_prior_bt: bool = False
+ failure_reason: SingleTestFailureReason = (
+ SingleTestFailureReason.UNINITIALIZED
+ )
+ result_message: str = ''
+ prior_nc_setup_quality_info: ConnectionSetupQualityInfo = dataclasses.field(
+ default_factory=ConnectionSetupQualityInfo
+ )
+ discoverer_sta_latency: datetime.timedelta = UNSET_LATENCY
+ file_transfer_nc_setup_quality_info: ConnectionSetupQualityInfo = (
+ dataclasses.field(default_factory=ConnectionSetupQualityInfo)
+ )
+ file_transfer_throughput_kbps: float = UNSET_THROUGHPUT_KBPS
+ advertiser_sta_latency: datetime.timedelta = UNSET_LATENCY
+ discoverer_sta_expected: bool = False
+ advertiser_wifi_expected: bool = False
+
+
+@dataclasses.dataclass(frozen=False)
+class NcPerformanceTestMetrics:
+ """Metrics data for quick start test."""
+
+ prior_bt_discovery_latencies: list[datetime.timedelta] = dataclasses.field(
+ default_factory=list[datetime.timedelta]
+ )
+ prior_bt_connection_latencies: list[datetime.timedelta] = dataclasses.field(
+ default_factory=list[datetime.timedelta]
+ )
+ discoverer_wifi_sta_latencies: list[datetime.timedelta] = dataclasses.field(
+ default_factory=list[datetime.timedelta]
+ )
+ file_transfer_discovery_latencies: list[datetime.timedelta] = (
+ dataclasses.field(default_factory=list[datetime.timedelta])
+ )
+ file_transfer_connection_latencies: list[datetime.timedelta] = (
+ dataclasses.field(default_factory=list[datetime.timedelta])
+ )
+ medium_upgrade_latencies: list[datetime.timedelta] = dataclasses.field(
+ default_factory=list[datetime.timedelta])
+ advertiser_wifi_sta_latencies: list[datetime.timedelta] = dataclasses.field(
+ default_factory=list[datetime.timedelta])
+ file_transfer_throughputs_kbps: list[float] = dataclasses.field(
+ default_factory=list[float])
+ upgraded_wifi_transfer_mediums: list[NearbyConnectionMedium] = (
+ dataclasses.field(default_factory=list[NearbyConnectionMedium]))
diff --git a/tests/bettertogether/betocq/nearby_connection_wrapper.py b/tests/bettertogether/betocq/nearby_connection_wrapper.py
new file mode 100644
index 000000000..8e0dd8fea
--- /dev/null
+++ b/tests/bettertogether/betocq/nearby_connection_wrapper.py
@@ -0,0 +1,417 @@
+# Copyright (C) 2024 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.
+
+"""Utils for handling Nearby Connection rpc."""
+
+import datetime
+import random
+import time
+
+from mobly import asserts
+from mobly import utils
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import callback_handler_v2
+from mobly.controllers.android_device_lib import snippet_client_v2
+from mobly.snippet import callback_event
+
+from betocq import nc_constants
+
+# This number should be large enough to cover advertising interval, firmware
+# scheduling timing interval and user action delay
+ADVERTISING_TO_DISCOVERY_MAX_DELAY_SEC = 4
+
+
+class NearbyConnectionWrapper:
+ """Wrapper for Nearby Connection Snippet Client Operations."""
+
+ def __init__(
+ self,
+ advertiser: android_device.AndroidDevice,
+ discoverer: android_device.AndroidDevice,
+ advertiser_nearby: snippet_client_v2.SnippetClientV2,
+ discoverer_nearby: snippet_client_v2.SnippetClientV2,
+ advertising_discovery_medium: nc_constants.NearbyMedium = (
+ nc_constants.NearbyMedium.BLE_ONLY
+ ),
+ connection_medium: nc_constants.NearbyMedium = (
+ nc_constants.NearbyMedium.BT_ONLY
+ ),
+ upgrade_medium: nc_constants.NearbyMedium = (
+ nc_constants.NearbyMedium.BT_ONLY
+ ),
+ ):
+ self.advertiser = advertiser
+ self.discoverer = discoverer
+ self.service_id = utils.rand_ascii_str(8)
+ self.advertising_discovery_medium = advertising_discovery_medium
+ self.connection_medium = connection_medium
+ self.upgrade_medium = upgrade_medium
+ self.discoverer_nearby = discoverer_nearby
+ self.advertiser_nearby = advertiser_nearby
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.UNINITIALIZED
+ )
+
+ self.connection_quality_info: nc_constants.ConnectionSetupQualityInfo = (
+ nc_constants.ConnectionSetupQualityInfo())
+
+ self._advertiser_connection_lifecycle_callback: (
+ callback_handler_v2.CallbackHandlerV2) = None
+ self._discoverer_endpoint_discovery_callback: (
+ callback_handler_v2.CallbackHandlerV2) = None
+ self._discoverer_connection_lifecycle_callback: (
+ callback_handler_v2.CallbackHandlerV2) = None
+ self._advertiser_payload_callback: (
+ callback_handler_v2.CallbackHandlerV2) = None
+ self._discoverer_payload_callback: (
+ callback_handler_v2.CallbackHandlerV2) = None
+ self._advertiser_endpoint_id: str = None
+ self._discoverer_endpoint_id: str = None
+
+ def start_advertising(self) -> None:
+ """Starts Nearby Connection advertising."""
+ advertiser_callback = self.advertiser_nearby.startAdvertising(
+ self.advertiser.serial,
+ self.service_id,
+ self.advertising_discovery_medium.value,
+ self.upgrade_medium.value,
+ )
+ self.advertiser.log.info(
+ f'Start advertising {self.advertising_discovery_medium.value}'
+ )
+ self._advertiser_connection_lifecycle_callback = advertiser_callback
+ self._advertiser_endpoint_id = self.advertiser_nearby.getLocalEndpointId()
+
+ def start_discovery(self, timeout: datetime.timedelta) -> None:
+ """Starts Nearby Connection discovery."""
+ self.discoverer.log.info(
+ f'Start discovery {self.advertising_discovery_medium.value}'
+ )
+ self._discoverer_endpoint_discovery_callback = (
+ self.discoverer_nearby.startDiscovery(
+ self.service_id, self.advertising_discovery_medium.value
+ )
+ )
+
+ endpoint_found_event = (
+ self._discoverer_endpoint_discovery_callback.waitAndGet(
+ 'onEndpointFound', timeout=timeout.total_seconds()
+ )
+ )
+ endpoint_info = endpoint_found_event.data['discoveredEndpointInfo']
+ self.connection_quality_info.discovery_latency = datetime.timedelta(
+ microseconds=endpoint_found_event.data['discoveryTimeNs'] / 1_000
+ )
+ asserts.assert_equal(
+ endpoint_info['endpointName'], self.advertiser.serial,
+ 'Received an unexpected endpoint during discovery: '
+ f'{endpoint_found_event}')
+
+ asserts.assert_equal(
+ endpoint_info['serviceId'], self.service_id,
+ f'Received an unexpected service id during discovery: '
+ f'{endpoint_found_event}')
+
+ def stop_advertising(self) -> None:
+ """Stops Nearby Connection advertising."""
+ self.advertiser_nearby.stopAdvertising()
+ self.advertiser.log.info('Stop advertising')
+
+ def stop_discovery(self) -> None:
+ """Stops Nearby Connection discovery."""
+ self.discoverer_nearby.stopDiscovery()
+ self.discoverer.log.info('Stop discovery')
+
+ def request_connection(
+ self,
+ medium_upgrade_type: nc_constants.MediumUpgradeType,
+ timeout: datetime.timedelta,
+ keep_alive_timeout_ms: int = nc_constants.KEEP_ALIVE_TIMEOUT_BT_MS,
+ keep_alive_interval_ms: int = nc_constants.KEEP_ALIVE_INTERVAL_BT_MS,
+ ) -> None:
+ """Requests Nearby Connection."""
+
+ self.discoverer.log.info(
+ 'Start connection request with keep_alive_timeout_ms'
+ f' {keep_alive_timeout_ms}'
+ )
+ self._discoverer_connection_lifecycle_callback = (
+ self.discoverer_nearby.requestConnection(
+ self.discoverer.serial,
+ self._advertiser_endpoint_id,
+ self.connection_medium.value,
+ self.upgrade_medium.value,
+ medium_upgrade_type.value,
+ keep_alive_timeout_ms,
+ keep_alive_interval_ms,
+ )
+ )
+
+ d_connection_init_event = (
+ self._discoverer_connection_lifecycle_callback.waitAndGet(
+ 'onConnectionInitiated', timeout.total_seconds()
+ )
+ )
+ self.connection_quality_info.connection_latency = datetime.timedelta(
+ microseconds=d_connection_init_event.data['connectionTimeNs'] / 1_000
+ )
+
+ d_connection_info = d_connection_init_event.data['connectionInfo']
+ asserts.assert_false(
+ d_connection_info['isIncomingConnection'],
+ f'Received an incoming connection: {d_connection_init_event}'
+ 'but expected an outgoing connection')
+
+ asserts.assert_equal(
+ d_connection_info['endpointName'],
+ self.advertiser.serial,
+ f'Received an unexpected endpoint: {d_connection_init_event}')
+
+ self._discoverer_endpoint_id = self.discoverer_nearby.getLocalEndpointId()
+
+ # wait for the advertiser connection initialized.
+ a_connection_init_event = (
+ self._advertiser_connection_lifecycle_callback.waitAndGet(
+ 'onConnectionInitiated', timeout=timeout.total_seconds()
+ )
+ )
+ a_connection_info = a_connection_init_event.data['connectionInfo']
+ asserts.assert_true(
+ a_connection_info['isIncomingConnection'],
+ f'Received an outgoing connection: {d_connection_init_event}'
+ 'but expected an incoming connection')
+
+ asserts.assert_equal(
+ a_connection_info['endpointName'],
+ self.discoverer.serial,
+ f'Received an unexpected endpoint: {a_connection_init_event}')
+
+ def accept_connection(
+ self, timeout: datetime.timedelta
+ ) -> None:
+ """Accepts Nearby Connection."""
+ self._advertiser_payload_callback = (
+ self.advertiser_nearby.acceptConnection(
+ self._discoverer_endpoint_id
+ )
+ )
+ self.advertiser.log.info('Start connection accept')
+ self._discoverer_payload_callback = (
+ self.discoverer_nearby.acceptConnection(
+ self._advertiser_endpoint_id
+ )
+ )
+ self.discoverer.log.info('Start connection accept')
+
+ advertiser_connection_event = (
+ self._advertiser_connection_lifecycle_callback.waitAndGet(
+ 'onConnectionResult', timeout=timeout.total_seconds()
+ )
+ )
+
+ asserts.assert_true(
+ advertiser_connection_event.data['isSuccess'],
+ f'Received an unsuccessful event: {advertiser_connection_event}')
+
+ asserts.assert_equal(
+ advertiser_connection_event.data['endpointId'],
+ self._discoverer_endpoint_id,
+ f'Received an unexpected endpoint: {advertiser_connection_event}')
+
+ discoverer_connection_event = (
+ self._discoverer_connection_lifecycle_callback.waitAndGet(
+ 'onConnectionResult', timeout=timeout.total_seconds()
+ )
+ )
+ asserts.assert_true(
+ discoverer_connection_event.data['isSuccess'],
+ f'Received an unsuccessful event: {discoverer_connection_event}')
+
+ asserts.assert_equal(
+ discoverer_connection_event.data['endpointId'],
+ self._advertiser_endpoint_id,
+ f'Received an unexpected endpoint: {discoverer_connection_event}')
+
+ if nc_constants.is_high_quality_medium(self.upgrade_medium):
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.WIFI_MEDIUM_UPGRADE
+ )
+ upgrade_start_time = datetime.datetime.now()
+ wait_high_quality = True
+ while wait_high_quality:
+ discoverer_medium_upgrade_event = self._discoverer_connection_lifecycle_callback.waitAndGet(
+ 'onBandwidthChanged',
+ nc_constants.CONNECTION_BANDWIDTH_CHANGED_TIMEOUT.total_seconds(),
+ )
+ self.discoverer.log.info(
+ f'medium upgrade to {discoverer_medium_upgrade_event.data}'
+ )
+ if discoverer_medium_upgrade_event.data['isHighBwQuality']:
+ wait_high_quality = False
+ self.connection_quality_info.medium_upgrade_latency = (
+ datetime.datetime.now() - upgrade_start_time)
+ self.connection_quality_info.upgrade_medium = (
+ nc_constants.NearbyConnectionMedium(
+ discoverer_medium_upgrade_event.data['medium']))
+ self.connection_quality_info.medium_upgrade_expected = True
+ self.discoverer.log.info(
+ f'upgraded to high quality medium: '
+ f'{self.connection_quality_info.upgrade_medium.name}')
+ else:
+ latency = datetime.datetime.now() - upgrade_start_time
+ if latency >= nc_constants.CONNECTION_BANDWIDTH_CHANGED_TIMEOUT:
+ raise TimeoutError('medium upgrade timeout')
+
+ def disconnect_endpoint(self) -> None:
+ """Disconnects Nearby Connection endpoint."""
+ if self:
+ self.discoverer_nearby.disconnectFromEndpoint(
+ self._advertiser_endpoint_id
+ )
+ self.discoverer.log.info(
+ f'Start disconnecting from endpoint: {self._advertiser_endpoint_id}'
+ )
+ else:
+ self.discoverer.log.info('no nearby connecty setup yet')
+ return nc_constants.OpResult(nc_constants.Result.SUCCESS)
+
+ if self._discoverer_connection_lifecycle_callback is not None:
+ disconnected_event = (
+ self._discoverer_connection_lifecycle_callback.waitAndGet(
+ 'onDisconnected',
+ timeout=nc_constants.DISCONNECTION_TIMEOUT.total_seconds(),
+ )
+ )
+ asserts.assert_equal(
+ disconnected_event.data['endpointId'],
+ self._advertiser_endpoint_id,
+ f'Receive unexpected event on disconnect: {disconnected_event}')
+ self.discoverer.log.info(
+ f'disconnected with endpoint: {self._advertiser_endpoint_id}'
+ )
+
+ def start_nearby_connection(
+ self,
+ timeouts: nc_constants.ConnectionSetupTimeouts,
+ medium_upgrade_type: nc_constants.MediumUpgradeType = nc_constants.MediumUpgradeType.DEFAULT,
+ keep_alive_timeout_ms: int = nc_constants.KEEP_ALIVE_TIMEOUT_BT_MS,
+ keep_alive_interval_ms: int = nc_constants.KEEP_ALIVE_INTERVAL_BT_MS,
+ ) -> None:
+ """Starts Nearby Connection between two Android devices."""
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.TARGET_START_ADVERTISING)
+ # Start advertising.
+ self.start_advertising()
+ # Add a random delay between adversting and discovery
+ # to mimic the random delay between two devices' user action
+ time.sleep(ADVERTISING_TO_DISCOVERY_MAX_DELAY_SEC * random.random())
+
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.SOURCE_START_DISCOVERY)
+ # Start discovery.
+ self.start_discovery(timeout=timeouts.discovery_timeout)
+
+ # Request connection.
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.SOURCE_REQUEST_CONNECTION)
+ self.request_connection(
+ medium_upgrade_type=medium_upgrade_type,
+ timeout=timeouts.connection_init_timeout,
+ keep_alive_timeout_ms=keep_alive_timeout_ms,
+ keep_alive_interval_ms=keep_alive_interval_ms)
+
+ # Stop discovery.
+ self.stop_discovery()
+
+ # Accept connection.
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.TARGET_ACCEPT_CONNECTION)
+ self.accept_connection(timeout=timeouts.connection_result_timeout)
+
+ # Stop advertising.
+ self.stop_advertising()
+ self.test_failure_reason = nc_constants.SingleTestFailureReason.SUCCESS
+
+ def transfer_file(
+ self,
+ file_size_kb: int,
+ timeout: datetime.timedelta,
+ payload_type: nc_constants.PayloadType,
+ ) -> float:
+ """Sends payloads and returns the transfer speed in kBS."""
+ try:
+ self.test_failure_reason = (
+ nc_constants.SingleTestFailureReason.FILE_TRANSFER_FAIL
+ )
+ transfer_speed_kbs = self._transfer_file(
+ file_size_kb, timeout, payload_type
+ )
+ self.test_failure_reason = nc_constants.SingleTestFailureReason.SUCCESS
+ finally:
+ # clean up
+ utils.concurrent_exec(
+ lambda nb: nb.transferFilesCleanup(),
+ param_list=[[self.discoverer_nearby], [self.advertiser_nearby]],
+ raise_on_exception=True)
+ return transfer_speed_kbs
+
+ def _transfer_file(
+ self, file_size_kb: int, timeout: datetime.timedelta,
+ payload_type: nc_constants.PayloadType
+ ) -> float:
+ """Sends payloads and returns the transfer speed in kBS."""
+ # Creates a file and send it to the advertiser.
+ file_name = utils.rand_ascii_str(8)
+ self.discoverer.log.info(
+ f'Start sending payloads with type: {payload_type.name}'
+ )
+ payload_id = self.discoverer_nearby.sendPayloadWithType(
+ self._advertiser_endpoint_id, file_name, file_size_kb, payload_type
+ )
+
+ # Waits for the advertiser received.
+ def on_receive(event: callback_event.CallbackEvent) -> bool:
+ return (
+ event.data['endpointId'] == self._discoverer_endpoint_id
+ and event.data['payload']['id'] == payload_id
+ )
+
+ asserts.assert_is_not_none(
+ self._advertiser_payload_callback,
+ 'No nearby connection is set up, advertiser payload cb is none.')
+ asserts.assert_is_not_none(
+ self._discoverer_payload_callback,
+ 'No nearby connection is set up, discoverer payload cb is none.')
+
+ self._advertiser_payload_callback.waitForEvent(
+ 'onPayloadReceived',
+ predicate=on_receive,
+ timeout=timeout.total_seconds())
+
+ # Waits for complete transfer.
+ self._advertiser_payload_callback.waitForEvent(
+ 'onPayloadTransferUpdate',
+ predicate=lambda event: event.data['update']['isSuccess'],
+ timeout=timeout.total_seconds())
+
+ payload_transfer_event = self._discoverer_payload_callback.waitForEvent(
+ 'onPayloadTransferUpdate',
+ predicate=lambda event: event.data['update']['isSuccess'],
+ timeout=timeout.total_seconds(),
+ )
+ self.advertiser.log.info('payload received')
+
+ transfer_time = datetime.timedelta(
+ microseconds=payload_transfer_event.data['transferTimeNs'] / 1_000)
+ return round(file_size_kb/transfer_time.total_seconds())
diff --git a/tests/bettertogether/betocq/setup_utils.py b/tests/bettertogether/betocq/setup_utils.py
new file mode 100644
index 000000000..b8ae7505c
--- /dev/null
+++ b/tests/bettertogether/betocq/setup_utils.py
@@ -0,0 +1,390 @@
+# Copyright (C) 2024 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.
+
+"""Android Nearby device setup."""
+
+import datetime
+import time
+from typing import Mapping
+
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import adb
+
+from betocq import gms_auto_updates_util
+
+WIFI_COUNTRYCODE_CONFIG_TIME_SEC = 3
+TOGGLE_AIRPLANE_MODE_WAIT_TIME_SEC = 2
+PH_FLAG_WRITE_WAIT_TIME_SEC = 3
+WIFI_DISCONNECTION_DELAY_SEC = 3
+ADB_RETRY_WAIT_TIME_SEC = 2
+
+_DISABLE_ENABLE_GMS_UPDATE_WAIT_TIME_SEC = 2
+
+LOG_TAGS = [
+ 'Nearby',
+ 'NearbyMessages',
+ 'NearbyDiscovery',
+ 'NearbyConnections',
+ 'NearbyMediums',
+ 'NearbySetup',
+]
+
+
+def set_country_code(
+ ad: android_device.AndroidDevice, country_code: str
+) -> None:
+ """Sets Wi-Fi and Telephony country code.
+
+ When you set the phone to EU or JP, the available 5GHz channels shrinks.
+ Some phones, like Pixel 2, can't use Wi-Fi Direct or Hotspot on 5GHz
+ in these countries. Pixel 3+ can, but only on some channels.
+ Not all of them. So, test Nearby Share or Nearby Connections without
+ Wi-Fi LAN to catch any bugs and make sure we don't break it later.
+
+ Args:
+ ad: AndroidDevice, Mobly Android Device.
+ country_code: WiFi and Telephony Country Code.
+ """
+ try:
+ _do_set_country_code(ad, country_code)
+ except adb.AdbError:
+ ad.log.exception(
+ f'Failed to set country code on device "{ad.serial}, try again.'
+ )
+ time.sleep(ADB_RETRY_WAIT_TIME_SEC)
+ _do_set_country_code(ad, country_code)
+
+
+def _do_set_country_code(
+ ad: android_device.AndroidDevice, country_code: str
+) -> None:
+ """Sets Wi-Fi and Telephony country code."""
+ if not ad.is_adb_root:
+ ad.log.info(
+ f'Skipped setting wifi country code on device "{ad.serial}" '
+ 'because we do not set country code on unrooted phone.'
+ )
+ return
+
+ ad.log.info(f'Set Wi-Fi and Telephony country code to {country_code}.')
+ ad.adb.shell('cmd wifi set-wifi-enabled disabled')
+ time.sleep(WIFI_COUNTRYCODE_CONFIG_TIME_SEC)
+ ad.adb.shell(
+ 'am broadcast -a com.android.internal.telephony.action.COUNTRY_OVERRIDE'
+ f' --es country {country_code}'
+ )
+ ad.adb.shell(f'cmd wifi force-country-code enabled {country_code}')
+ enable_airplane_mode(ad)
+ time.sleep(WIFI_COUNTRYCODE_CONFIG_TIME_SEC)
+ disable_airplane_mode(ad)
+ ad.adb.shell('cmd wifi set-wifi-enabled enabled')
+ telephony_country_code = (
+ ad.adb.shell('dumpsys wifi | grep mTelephonyCountryCode')
+ .decode('utf-8')
+ .strip()
+ )
+ ad.log.info(f'Telephony country code: {telephony_country_code}')
+
+
+def enable_logs(ad: android_device.AndroidDevice) -> None:
+ """Enables Nearby related logs."""
+ ad.log.info('Enable Nearby loggings.')
+ for tag in LOG_TAGS:
+ ad.adb.shell(f'setprop log.tag.{tag} VERBOSE')
+
+
+def grant_manage_external_storage_permission(
+ ad: android_device.AndroidDevice, package_name: str
+) -> None:
+ """Grants MANAGE_EXTERNAL_STORAGE permission to Nearby snippet."""
+ try:
+ _do_grant_manage_external_storage_permission(ad, package_name)
+ except adb.AdbError:
+ ad.log.exception(
+ 'Failed to grant MANAGE_EXTERNAL_STORAGE permission on device'
+ f' "{ad.serial}", try again.'
+ )
+ time.sleep(ADB_RETRY_WAIT_TIME_SEC)
+ _do_grant_manage_external_storage_permission(ad, package_name)
+
+
+def _do_grant_manage_external_storage_permission(
+ ad: android_device.AndroidDevice, package_name: str
+) -> None:
+ """Grants MANAGE_EXTERNAL_STORAGE permission to Nearby snippet."""
+ build_version_sdk = int(ad.build_info['build_version_sdk'])
+ if build_version_sdk < 30:
+ return
+ ad.log.info(
+ f'Grant MANAGE_EXTERNAL_STORAGE permission on device "{ad.serial}".'
+ )
+ _grant_manage_external_storage_permission(ad, package_name)
+
+
+def dump_gms_version(ad: android_device.AndroidDevice) -> Mapping[str, str]:
+ """Dumps GMS version from dumpsys to sponge properties."""
+ try:
+ gms_version = _do_dump_gms_version(ad)
+ except adb.AdbError:
+ ad.log.exception(
+ f'Failed to dump GMS version on device "{ad.serial}", try again.'
+ )
+ time.sleep(ADB_RETRY_WAIT_TIME_SEC)
+ gms_version = _do_dump_gms_version(ad)
+ return gms_version
+
+
+def _do_dump_gms_version(ad: android_device.AndroidDevice) -> Mapping[str, str]:
+ """Dumps GMS version from dumpsys to sponge properties."""
+ out = (
+ ad.adb.shell(
+ 'dumpsys package com.google.android.gms | grep "versionCode="'
+ )
+ .decode('utf-8')
+ .strip()
+ )
+ return {f'GMS core version on {ad.serial}': out}
+
+
+def toggle_airplane_mode(ad: android_device.AndroidDevice) -> None:
+ """Toggles airplane mode on the given device."""
+ ad.log.info('turn on airplane mode')
+ enable_airplane_mode(ad)
+ ad.log.info('turn off airplane mode')
+ disable_airplane_mode(ad)
+
+
+def connect_to_wifi_sta_till_success(
+ ad: android_device.AndroidDevice, wifi_ssid: str, wifi_password: str
+) -> datetime.timedelta:
+ """Connecting to the specified wifi STA/AP."""
+ ad.log.info('Start connecting to wifi STA/AP')
+ wifi_connect_start = datetime.datetime.now()
+ if not wifi_password:
+ wifi_password = None
+ connect_to_wifi(ad, wifi_ssid, wifi_password)
+ return datetime.datetime.now() - wifi_connect_start
+
+
+def connect_to_wifi(
+ ad: android_device.AndroidDevice,
+ ssid: str,
+ password: str | None = None,
+) -> None:
+ if not ad.nearby.wifiIsEnabled():
+ ad.nearby.wifiEnable()
+ # return until the wifi is connected.
+ ad.nearby.wifiConnectSimple(ssid, password)
+
+
+def disconnect_from_wifi(ad: android_device.AndroidDevice) -> None:
+ if not ad.is_adb_root:
+ ad.log.info("Can't clear wifi network in non-rooted device")
+ return
+ ad.nearby.wifiClearConfiguredNetworks()
+ time.sleep(WIFI_DISCONNECTION_DELAY_SEC)
+
+
+def _grant_manage_external_storage_permission(
+ ad: android_device.AndroidDevice, package_name: str
+) -> None:
+ """Grants MANAGE_EXTERNAL_STORAGE permission to Nearby snippet.
+
+ This permission will not grant automatically by '-g' option of adb install,
+ you can check the all permission granted by:
+ am start -a android.settings.APPLICATION_DETAILS_SETTINGS
+ -d package:{YOUR_PACKAGE}
+
+ Reference for MANAGE_EXTERNAL_STORAGE:
+ https://developer.android.com/training/data-storage/manage-all-files
+
+ This permission will reset to default "Allow access to media only" after
+ reboot if you never grant "Allow management of all files" through system UI.
+ The appops command and MANAGE_EXTERNAL_STORAGE only available on API 30+.
+
+ Args:
+ ad: AndroidDevice, Mobly Android Device.
+ package_name: The nearbu snippet package name.
+ """
+ try:
+ ad.adb.shell(
+ f'appops set --uid {package_name} MANAGE_EXTERNAL_STORAGE allow'
+ )
+ except adb.Error:
+ ad.log.info('Failed to grant MANAGE_EXTERNAL_STORAGE permission.')
+
+
+def enable_airplane_mode(ad: android_device.AndroidDevice) -> None:
+ """Enables airplane mode on the given device."""
+ try:
+ _do_enable_airplane_mode(ad)
+ except adb.AdbError:
+ ad.log.exception(
+ f'Failed to enable airplane mode on device "{ad.serial}", try again.'
+ )
+ time.sleep(ADB_RETRY_WAIT_TIME_SEC)
+ _do_enable_airplane_mode(ad)
+
+
+def _do_enable_airplane_mode(ad: android_device.AndroidDevice) -> None:
+ if (ad.is_adb_root):
+ ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '1'])
+ ad.adb.shell([
+ 'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez',
+ 'state', 'true'
+ ])
+ ad.adb.shell(['svc', 'wifi', 'disable'])
+ ad.adb.shell(['svc', 'bluetooth', 'disable'])
+ time.sleep(TOGGLE_AIRPLANE_MODE_WAIT_TIME_SEC)
+
+
+def disable_airplane_mode(ad: android_device.AndroidDevice) -> None:
+ """Disables airplane mode on the given device."""
+ try:
+ _do_disable_airplane_mode(ad)
+ except adb.AdbError:
+ ad.log.exception(
+ f'Failed to disable airplane mode on device "{ad.serial}", try again.'
+ )
+ time.sleep(ADB_RETRY_WAIT_TIME_SEC)
+ _do_disable_airplane_mode(ad)
+
+
+def _do_disable_airplane_mode(ad: android_device.AndroidDevice) -> None:
+ if (ad.is_adb_root):
+ ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '0'])
+ ad.adb.shell([
+ 'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez',
+ 'state', 'false'
+ ])
+ ad.adb.shell(['svc', 'wifi', 'enable'])
+ ad.adb.shell(['svc', 'bluetooth', 'enable'])
+ time.sleep(TOGGLE_AIRPLANE_MODE_WAIT_TIME_SEC)
+
+
+def check_if_ph_flag_committed(
+ ad: android_device.AndroidDevice,
+ pname: str,
+ flag_name: str,
+) -> bool:
+ """Check if P/H flag is committed."""
+ sql_str = (
+ 'sqlite3 /data/data/com.google.android.gms/databases/phenotype.db'
+ ' "select name, quote(coalesce(intVal, boolVal, floatVal, stringVal,'
+ ' extensionVal)) from FlagOverrides where committed=1 AND'
+ f' packageName=\'{pname}\';"'
+ )
+ flag_result = ad.adb.shell(sql_str).decode('utf-8').strip()
+ return flag_name in flag_result
+
+
+def write_ph_flag(
+ ad: android_device.AndroidDevice,
+ pname: str,
+ flag_name: str,
+ flag_type: str,
+ flag_value: str,
+) -> None:
+ """Write P/H flag."""
+ ad.adb.shell(
+ 'am broadcast -a "com.google.android.gms.phenotype.FLAG_OVERRIDE" '
+ f'--es package "{pname}" --es user "*" '
+ f'--esa flags "{flag_name}" '
+ f'--esa types "{flag_type}" --esa values "{flag_value}" '
+ 'com.google.android.gms'
+ )
+ time.sleep(PH_FLAG_WRITE_WAIT_TIME_SEC)
+
+
+def check_and_try_to_write_ph_flag(
+ ad: android_device.AndroidDevice,
+ pname: str,
+ flag_name: str,
+ flag_type: str,
+ flag_value: str,
+) -> None:
+ """Check and try to enable the given flag on the given device."""
+ if(not ad.is_adb_root):
+ ad.log.info(
+ "Can't read or write P/H flag value in non-rooted device. Use Mobile"
+ ' Utility app to config instead.'
+ )
+ return
+
+ if check_if_ph_flag_committed(ad, pname, flag_name):
+ ad.log.info(f'{flag_name} is already committed.')
+ return
+ ad.log.info(f'write {flag_name}.')
+ write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
+
+ if check_if_ph_flag_committed(ad, pname, flag_name):
+ ad.log.info(f'{flag_name} is configured successfully.')
+ else:
+ ad.log.info(f'failed to configure {flag_name}.')
+
+
+def enable_bluetooth_multiplex(ad: android_device.AndroidDevice) -> None:
+ """Enable bluetooth multiplex on the given device."""
+ pname = 'com.google.android.gms.nearby'
+ flag_name = 'mediums_supports_bluetooth_multiplex_socket'
+ flag_type = 'boolean'
+ flag_value = 'true'
+ check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
+
+
+def enable_wifi_aware(ad: android_device.AndroidDevice) -> None:
+ """Enable wifi aware on the given device."""
+ pname = 'com.google.android.gms.nearby'
+ flag_name = 'mediums_supports_wifi_aware'
+ flag_type = 'boolean'
+ flag_value = 'true'
+
+ check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
+
+
+def disable_redaction(ad: android_device.AndroidDevice) -> None:
+ """Disable info log redaction on the given device."""
+ pname = 'com.google.android.gms'
+ flag_name = 'ClientLogging__enable_info_log_redaction'
+ flag_type = 'boolean'
+ flag_value = 'false'
+
+ check_and_try_to_write_ph_flag(ad, pname, flag_name, flag_type, flag_value)
+
+
+def install_apk(ad: android_device.AndroidDevice, apk_path: str) -> None:
+ """Installs the apk on the given device."""
+ ad.adb.install(['-r', '-g', '-t', apk_path])
+
+
+def disable_gms_auto_updates(ad: android_device.AndroidDevice) -> None:
+ """Disable GMS auto updates on the given device."""
+ if not ad.is_adb_root:
+ ad.log.warning(
+ 'You should disable the play store auto updates manually on a'
+ 'unrooted device, otherwise the test may be broken unexpected')
+ ad.log.info('try to disable GMS Auto Updates.')
+ gms_auto_updates_util.GmsAutoUpdatesUtil(ad).disable_gms_auto_updates()
+ time.sleep(_DISABLE_ENABLE_GMS_UPDATE_WAIT_TIME_SEC)
+
+
+def enable_gms_auto_updates(ad: android_device.AndroidDevice) -> None:
+ """Enable GMS auto updates on the given device."""
+ if not ad.is_adb_root:
+ ad.log.warning(
+ 'You may enable the play store auto updates manually on a'
+ 'unrooted device after test.')
+ ad.log.info('try to enable GMS Auto Updates.')
+ gms_auto_updates_util.GmsAutoUpdatesUtil(ad).enable_gms_auto_updates()
+ time.sleep(_DISABLE_ENABLE_GMS_UPDATE_WAIT_TIME_SEC)
diff --git a/tests/bettertogether/betocq/version.py b/tests/bettertogether/betocq/version.py
new file mode 100644
index 000000000..a21f32a58
--- /dev/null
+++ b/tests/bettertogether/betocq/version.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2024 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.
+
+"""Define the Beto CQ test script version."""
+
+
+TEST_SCRIPT_VERSION = '2.0.0'
+
+# VERSION_LOG (only add new description for new version, keep the history log)
+# '2.0.0': 'initial version'