Merge "Add CtsConnectivityMultiDevicesTestCases for hotspot" into main
This commit is contained in:
commit
9c3c120c6c
7 changed files with 445 additions and 1 deletions
|
|
@ -116,7 +116,10 @@ class ConnectUtil(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun connectToWifiConfig(config: WifiConfiguration) {
|
||||
// Suppress warning because WifiManager methods to connect to a config are
|
||||
// documented not to be deprecated for privileged users.
|
||||
@Suppress("DEPRECATION")
|
||||
fun connectToWifiConfig(config: WifiConfiguration) {
|
||||
repeat(MAX_WIFI_CONNECT_RETRIES) {
|
||||
val error = runAsShell(permission.NETWORK_SETTINGS) {
|
||||
val listener = ConnectWifiListener()
|
||||
|
|
|
|||
42
tests/cts/multidevices/Android.bp
Normal file
42
tests/cts/multidevices/Android.bp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// 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"],
|
||||
}
|
||||
|
||||
python_test_host {
|
||||
name: "CtsConnectivityMultiDevicesTestCases",
|
||||
main: "connectivity_multi_devices_test.py",
|
||||
srcs: ["connectivity_multi_devices_test.py"],
|
||||
libs: [
|
||||
"mobly",
|
||||
],
|
||||
test_suites: [
|
||||
"cts",
|
||||
"general-tests",
|
||||
],
|
||||
test_options: {
|
||||
unit_test: false,
|
||||
},
|
||||
data: [
|
||||
// Package the snippet with the mobly test
|
||||
":connectivity_multi_devices_snippet",
|
||||
],
|
||||
version: {
|
||||
py3: {
|
||||
embedded_launcher: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
43
tests/cts/multidevices/AndroidTest.xml
Normal file
43
tests/cts/multidevices/AndroidTest.xml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<configuration description="Config for CTS Connectivity multi devices test cases">
|
||||
<option name="test-suite-tag" value="cts" />
|
||||
<option name="config-descriptor:metadata" key="component" value="networking" />
|
||||
<option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
|
||||
<option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
|
||||
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
|
||||
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
|
||||
|
||||
<device name="device1">
|
||||
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
|
||||
<option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
|
||||
</target_preparer>
|
||||
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
|
||||
</target_preparer>
|
||||
</device>
|
||||
<device name="device2">
|
||||
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
|
||||
<option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
|
||||
</target_preparer>
|
||||
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
|
||||
</target_preparer>
|
||||
</device>
|
||||
|
||||
<test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
|
||||
<!-- The mobly-par-file-name should match the module name -->
|
||||
<option name="mobly-par-file-name" value="CtsConnectivityMultiDevicesTestCases" />
|
||||
<!-- Timeout limit in milliseconds for all test cases of the python binary -->
|
||||
<option name="mobly-test-timeout" value="180000" />
|
||||
</test>
|
||||
</configuration>
|
||||
|
||||
110
tests/cts/multidevices/connectivity_multi_devices_test.py
Normal file
110
tests/cts/multidevices/connectivity_multi_devices_test.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Lint as: python3
|
||||
"""Connectivity multi devices tests."""
|
||||
import base64
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from mobly import asserts
|
||||
from mobly import base_test
|
||||
from mobly import test_runner
|
||||
from mobly import utils
|
||||
from mobly.controllers import android_device
|
||||
|
||||
CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
|
||||
|
||||
|
||||
class UpstreamType:
|
||||
CELLULAR = 1
|
||||
WIFI = 2
|
||||
|
||||
|
||||
class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
|
||||
|
||||
def setup_class(self):
|
||||
# Declare that two Android devices are needed.
|
||||
self.clientDevice, self.serverDevice = self.register_controller(
|
||||
android_device, min_number=2
|
||||
)
|
||||
|
||||
def setup_device(device):
|
||||
device.load_snippet(
|
||||
"connectivity_multi_devices_snippet",
|
||||
CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
|
||||
)
|
||||
|
||||
# Set up devices in parallel to save time.
|
||||
utils.concurrent_exec(
|
||||
setup_device,
|
||||
((self.clientDevice,), (self.serverDevice,)),
|
||||
max_workers=2,
|
||||
raise_on_exception=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_uuid32_base64():
|
||||
"""Generates a UUID32 and encodes it in Base64.
|
||||
|
||||
Returns:
|
||||
str: The Base64-encoded UUID32 string. Which is 22 characters.
|
||||
"""
|
||||
return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
|
||||
|
||||
def _do_test_hotspot_for_upstream_type(self, upstream_type):
|
||||
"""Test hotspot with the specified upstream type.
|
||||
|
||||
This test create a hotspot, make the client connect
|
||||
to it, and verify the packet is forwarded by the hotspot.
|
||||
"""
|
||||
server = self.serverDevice.connectivity_multi_devices_snippet
|
||||
client = self.clientDevice.connectivity_multi_devices_snippet
|
||||
|
||||
# Assert pre-conditions specific to each upstream type.
|
||||
asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
|
||||
asserts.skip_if(
|
||||
not server.hasHotspotFeature(), "Server requires hotspot feature"
|
||||
)
|
||||
if upstream_type == UpstreamType.CELLULAR:
|
||||
asserts.skip_if(
|
||||
not server.hasTelephonyFeature(), "Server requires Telephony feature"
|
||||
)
|
||||
server.requestCellularAndEnsureDefault()
|
||||
elif upstream_type == UpstreamType.WIFI:
|
||||
asserts.skip_if(
|
||||
not server.isStaApConcurrencySupported(),
|
||||
"Server requires Wifi AP + STA concurrency",
|
||||
)
|
||||
server.ensureWifiIsDefault()
|
||||
else:
|
||||
raise ValueError(f"Invalid upstream type: {upstream_type}")
|
||||
|
||||
# Generate ssid/passphrase with random characters to make sure nearby devices won't
|
||||
# connect unexpectedly. Note that total length of ssid cannot go over 32.
|
||||
testSsid = "HOTSPOT-" + self.generate_uuid32_base64()
|
||||
testPassphrase = self.generate_uuid32_base64()
|
||||
|
||||
try:
|
||||
# Create a hotspot with fixed SSID and password.
|
||||
server.startHotspot(testSsid, testPassphrase)
|
||||
|
||||
# Make the client connects to the hotspot.
|
||||
client.connectToWifi(testSsid, testPassphrase, True)
|
||||
|
||||
finally:
|
||||
if upstream_type == UpstreamType.CELLULAR:
|
||||
server.unrequestCellular()
|
||||
# Teardown the hotspot.
|
||||
server.stopAllTethering()
|
||||
|
||||
def test_hotspot_upstream_wifi(self):
|
||||
self._do_test_hotspot_for_upstream_type(UpstreamType.WIFI)
|
||||
|
||||
def test_hotspot_upstream_cellular(self):
|
||||
self._do_test_hotspot_for_upstream_type(UpstreamType.CELLULAR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Take test args
|
||||
if "--" in sys.argv:
|
||||
index = sys.argv.index("--")
|
||||
sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
|
||||
test_runner.main()
|
||||
37
tests/cts/multidevices/snippet/Android.bp
Normal file
37
tests/cts/multidevices/snippet/Android.bp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// 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"],
|
||||
}
|
||||
|
||||
android_test_helper_app {
|
||||
name: "connectivity_multi_devices_snippet",
|
||||
defaults: [
|
||||
"ConnectivityTestsLatestSdkDefaults",
|
||||
"cts_defaults",
|
||||
"framework-connectivity-test-defaults",
|
||||
],
|
||||
srcs: [
|
||||
"ConnectivityMultiDevicesSnippet.kt",
|
||||
],
|
||||
manifest: "AndroidManifest.xml",
|
||||
static_libs: [
|
||||
"androidx.test.runner",
|
||||
"mobly-snippet-lib",
|
||||
"cts-net-utils",
|
||||
],
|
||||
platform_apis: true,
|
||||
min_sdk_version: "30", // R
|
||||
}
|
||||
39
tests/cts/multidevices/snippet/AndroidManifest.xml
Normal file
39
tests/cts/multidevices/snippet/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.snippet.connectivity">
|
||||
<!-- Declare the minimum Android SDK version and internet permission,
|
||||
which are required by Mobly Snippet Lib since it uses network socket. -->
|
||||
<uses-sdk android:minSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application>
|
||||
<!-- Add any classes that implement the Snippet interface as meta-data, whose
|
||||
value is a comma-separated string, each section being the package path
|
||||
of a snippet class -->
|
||||
<meta-data
|
||||
android:name="mobly-snippets"
|
||||
android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
|
||||
</application>
|
||||
<!-- Add an instrumentation tag so that the app can be launched through an
|
||||
instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
|
||||
is derived from `AndroidJUnitRunner`, and is required to use the
|
||||
Mobly Snippet Lib. -->
|
||||
<instrumentation
|
||||
android:name="com.google.android.mobly.snippet.SnippetRunner"
|
||||
android:targetPackage="com.google.snippet.connectivity" />
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 com.google.snippet.connectivity
|
||||
|
||||
import android.Manifest.permission.OVERRIDE_WIFI_CONFIG
|
||||
import android.content.pm.PackageManager.FEATURE_TELEPHONY
|
||||
import android.content.pm.PackageManager.FEATURE_WIFI
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.net.NetworkRequest
|
||||
import android.net.cts.util.CtsNetUtils
|
||||
import android.net.cts.util.CtsTetheringUtils
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSpecifier
|
||||
import android.net.wifi.WifiSsid
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.android.testutils.ConnectUtil
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.Available
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
|
||||
import com.android.testutils.TestableNetworkCallback
|
||||
import com.android.testutils.runAsShell
|
||||
import com.google.android.mobly.snippet.Snippet
|
||||
import com.google.android.mobly.snippet.rpc.Rpc
|
||||
|
||||
class ConnectivityMultiDevicesSnippet : Snippet {
|
||||
private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
|
||||
private val wifiManager = context.getSystemService(WifiManager::class.java)!!
|
||||
private val cm = context.getSystemService(ConnectivityManager::class.java)!!
|
||||
private val pm = context.packageManager
|
||||
private val ctsNetUtils = CtsNetUtils(context)
|
||||
private val ctsTetheringUtils = CtsTetheringUtils(context)
|
||||
private var oldSoftApConfig: SoftApConfiguration? = null
|
||||
|
||||
@Rpc(description = "Check whether the device has wifi feature.")
|
||||
fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
|
||||
|
||||
@Rpc(description = "Check whether the device has telephony feature.")
|
||||
fun hasTelephonyFeature() = pm.hasSystemFeature(FEATURE_TELEPHONY)
|
||||
|
||||
@Rpc(description = "Check whether the device supporters AP + STA concurrency.")
|
||||
fun isStaApConcurrencySupported() {
|
||||
wifiManager.isStaApConcurrencySupported()
|
||||
}
|
||||
|
||||
@Rpc(description = "Request cellular connection and ensure it is the default network.")
|
||||
fun requestCellularAndEnsureDefault() {
|
||||
ctsNetUtils.disableWifi()
|
||||
val network = ctsNetUtils.connectToCell()
|
||||
ctsNetUtils.expectNetworkIsSystemDefault(network)
|
||||
}
|
||||
|
||||
@Rpc(description = "Unrequest cellular connection.")
|
||||
fun unrequestCellular() {
|
||||
ctsNetUtils.disconnectFromCell()
|
||||
}
|
||||
|
||||
@Rpc(description = "Ensure any wifi is connected and is the default network.")
|
||||
fun ensureWifiIsDefault() {
|
||||
val network = ctsNetUtils.ensureWifiConnected()
|
||||
ctsNetUtils.expectNetworkIsSystemDefault(network)
|
||||
}
|
||||
|
||||
@Rpc(description = "Connect to specified wifi network.")
|
||||
// Suppress warning because WifiManager methods to connect to a config are
|
||||
// documented not to be deprecated for privileged users.
|
||||
@Suppress("DEPRECATION")
|
||||
fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Network {
|
||||
val specifier = WifiNetworkSpecifier.Builder()
|
||||
.setSsid(ssid)
|
||||
.setWpa2Passphrase(passphrase)
|
||||
.setBand(ScanResult.WIFI_BAND_24_GHZ)
|
||||
.build()
|
||||
val wifiConfig = WifiConfiguration()
|
||||
wifiConfig.SSID = "\"" + ssid + "\""
|
||||
wifiConfig.preSharedKey = "\"" + passphrase + "\""
|
||||
wifiConfig.hiddenSSID = true
|
||||
wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK)
|
||||
wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
|
||||
wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
|
||||
|
||||
// Register network callback for the specific wifi.
|
||||
val networkCallback = TestableNetworkCallback()
|
||||
val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
|
||||
.setNetworkSpecifier(specifier)
|
||||
.build()
|
||||
cm.registerNetworkCallback(wifiRequest, networkCallback)
|
||||
|
||||
try {
|
||||
// Add the test configuration and connect to it.
|
||||
val connectUtil = ConnectUtil(context)
|
||||
connectUtil.connectToWifiConfig(wifiConfig)
|
||||
|
||||
val event = networkCallback.expect<Available>()
|
||||
if (requireValidation) {
|
||||
networkCallback.eventuallyExpect<CapabilitiesChanged> {
|
||||
it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
}
|
||||
return event.network
|
||||
} finally {
|
||||
cm.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@Rpc(description = "Check whether the device supports hotspot feature.")
|
||||
fun hasHotspotFeature(): Boolean {
|
||||
val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
|
||||
try {
|
||||
return tetheringCallback.isWifiTetheringSupported(context)
|
||||
} finally {
|
||||
ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@Rpc(description = "Start a hotspot with given SSID and passphrase.")
|
||||
fun startHotspot(ssid: String, passphrase: String) {
|
||||
// Store old config.
|
||||
runAsShell(OVERRIDE_WIFI_CONFIG) {
|
||||
oldSoftApConfig = wifiManager.getSoftApConfiguration()
|
||||
}
|
||||
|
||||
val softApConfig = SoftApConfiguration.Builder()
|
||||
.setWifiSsid(WifiSsid.fromBytes(ssid.toByteArray()))
|
||||
.setPassphrase(passphrase, SECURITY_TYPE_WPA2_PSK)
|
||||
.setBand(SoftApConfiguration.BAND_2GHZ)
|
||||
.build()
|
||||
runAsShell(OVERRIDE_WIFI_CONFIG) {
|
||||
wifiManager.setSoftApConfiguration(softApConfig)
|
||||
}
|
||||
val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
|
||||
try {
|
||||
tetheringCallback.expectNoTetheringActive()
|
||||
ctsTetheringUtils.startWifiTethering(tetheringCallback)
|
||||
} finally {
|
||||
ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@Rpc(description = "Stop all tethering.")
|
||||
fun stopAllTethering() {
|
||||
ctsTetheringUtils.stopAllTethering()
|
||||
|
||||
// Restore old config.
|
||||
oldSoftApConfig?.let {
|
||||
runAsShell(OVERRIDE_WIFI_CONFIG) {
|
||||
wifiManager.setSoftApConfiguration(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue