Add ConnectivityCheckTargetPreparer
ConnectivityCheckTargetPreparer is a tradefed target preparer that uses
the connectivitychecker app to verify device configuration, before
running any test.
The connectivitychecker app verifies that the device has a
pre-configured wifi configuration and can connect to it (except for
virtual devices where it may create the configuration itself), and
verifies that the device has a data-enabled SIM card inserted.
Checks are skipped if the device is not wifi- or mobile data-capable,
and can be skipped for local testing by running with:
atest X -- --test-arg \
com.android.testutils.ConnectivityCheckTargetPreparer:disable:true
Test: atest CtsNetTestCasesLatestSdk
Change-Id: I5b6d34a6c393863af23af57ff026b15973e9e784
This commit is contained in:
parent
c02a31e57a
commit
c02dcd721a
6 changed files with 417 additions and 0 deletions
|
|
@ -61,3 +61,14 @@ java_library {
|
|||
"kotlin-test"
|
||||
]
|
||||
}
|
||||
|
||||
java_test_host {
|
||||
name: "net-tests-utils-host-common",
|
||||
srcs: [
|
||||
"host/**/*.java",
|
||||
"host/**/*.kt",
|
||||
],
|
||||
libs: ["tradefed"],
|
||||
test_suites: ["device-tests", "general-tests", "cts", "mts"],
|
||||
data: [":ConnectivityChecker"],
|
||||
}
|
||||
|
|
|
|||
29
staticlibs/testutils/app/connectivitychecker/Android.bp
Normal file
29
staticlibs/testutils/app/connectivitychecker/Android.bp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2021 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
android_test_helper_app {
|
||||
name: "ConnectivityChecker",
|
||||
srcs: ["src/**/*.kt"],
|
||||
sdk_version: "system_current",
|
||||
// Allow running the test on any device with SDK Q+, even when built from a branch that uses
|
||||
// an unstable SDK, by targeting a stable SDK regardless of the build SDK.
|
||||
min_sdk_version: "29",
|
||||
target_sdk_version: "30",
|
||||
static_libs: [
|
||||
"androidx.test.rules",
|
||||
"modules-utils-build_system",
|
||||
"net-tests-utils",
|
||||
],
|
||||
host_required: ["net-tests-utils-host-common"],
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2021 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.testutils.connectivitychecker">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<!-- For wifi scans -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="com.android.testutils.connectivitychecker"
|
||||
android:label="Connectivity checker target preparer" />
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.testutils.connectivitychecker
|
||||
|
||||
import android.content.pm.PackageManager.FEATURE_TELEPHONY
|
||||
import android.content.pm.PackageManager.FEATURE_WIFI
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.android.testutils.ConnectUtil
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ConnectivityCheckTest {
|
||||
val context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
val pm by lazy { context.packageManager }
|
||||
|
||||
@Test
|
||||
fun testCheckDeviceSetup() {
|
||||
checkWifiSetup()
|
||||
checkTelephonySetup()
|
||||
}
|
||||
|
||||
private fun checkWifiSetup() {
|
||||
if (!pm.hasSystemFeature(FEATURE_WIFI)) return
|
||||
ConnectUtil(context).ensureWifiConnected()
|
||||
}
|
||||
|
||||
private fun checkTelephonySetup() {
|
||||
if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
|
||||
val tm = context.getSystemService(TelephonyManager::class.java)
|
||||
?: fail("Could not get telephony service")
|
||||
|
||||
val commonError = "Check the test bench. To run the tests anyway for quick & dirty local " +
|
||||
"testing, you can use atest X -- " +
|
||||
"--test-arg com.android.testutils.ConnectivityCheckTargetPreparer:disable:true"
|
||||
// Do not use assertEquals: it outputs "expected X, was Y", which looks like a test failure
|
||||
if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
|
||||
fail("The device has no SIM card inserted. " + commonError)
|
||||
} else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
|
||||
fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
|
||||
commonError)
|
||||
}
|
||||
assertTrue(tm.isDataConnectivityPossible,
|
||||
"The device is not setup with a SIM card that supports data connectivity. " +
|
||||
commonError)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.testutils
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.net.NetworkRequest
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
private const val MAX_WIFI_CONNECT_RETRIES = 10
|
||||
private const val WIFI_CONNECT_INTERVAL_MS = 500L
|
||||
private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L
|
||||
|
||||
// Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
|
||||
// the error code constants are not (b/204277752)
|
||||
private const val WIFI_ERROR_IN_PROGRESS = 1
|
||||
private const val WIFI_ERROR_BUSY = 2
|
||||
|
||||
class ConnectUtil(private val context: Context) {
|
||||
private val TAG = ConnectUtil::class.java.simpleName
|
||||
|
||||
private val cm = context.getSystemService(ConnectivityManager::class.java)
|
||||
?: fail("Could not find ConnectivityManager")
|
||||
private val wifiManager = context.getSystemService(WifiManager::class.java)
|
||||
?: fail("Could not find WifiManager")
|
||||
|
||||
fun ensureWifiConnected(): Network {
|
||||
val callback = TestableNetworkCallback()
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder()
|
||||
.addTransportType(TRANSPORT_WIFI)
|
||||
.build(), callback)
|
||||
|
||||
try {
|
||||
val connInfo = wifiManager.connectionInfo
|
||||
if (connInfo == null || connInfo.networkId == -1) {
|
||||
clearWifiBlocklist()
|
||||
val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
|
||||
// Read the output stream to ensure the command has completed
|
||||
ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
|
||||
val config = getOrCreateWifiConfiguration()
|
||||
connectToWifiConfig(config)
|
||||
}
|
||||
val cb = callback.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>(
|
||||
timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
|
||||
|
||||
assertNotNull(cb, "Could not connect to a wifi access point within " +
|
||||
"$WIFI_CONNECT_INTERVAL_MS ms. Check that the test device has a wifi network " +
|
||||
"configured, and that the test access point is functioning properly.")
|
||||
return cb.network
|
||||
} finally {
|
||||
cm.unregisterNetworkCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToWifiConfig(config: WifiConfiguration) {
|
||||
repeat(MAX_WIFI_CONNECT_RETRIES) {
|
||||
val error = runAsShell(permission.NETWORK_SETTINGS) {
|
||||
val listener = ConnectWifiListener()
|
||||
wifiManager.connect(config, listener)
|
||||
listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
} ?: return // Connect succeeded
|
||||
|
||||
// Only retry for IN_PROGRESS and BUSY
|
||||
if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
|
||||
fail("Failed to connect to " + config.SSID + ": " + error)
|
||||
}
|
||||
Log.w(TAG, "connect failed with $error; waiting before retry")
|
||||
SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
|
||||
}
|
||||
fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
|
||||
}
|
||||
|
||||
private class ConnectWifiListener : WifiManager.ActionListener {
|
||||
/**
|
||||
* Future completed when the connect process ends. Provides the error code or null if none.
|
||||
*/
|
||||
val connectFuture = CompletableFuture<Int?>()
|
||||
override fun onSuccess() {
|
||||
connectFuture.complete(null)
|
||||
}
|
||||
|
||||
override fun onFailure(reason: Int) {
|
||||
connectFuture.complete(reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrCreateWifiConfiguration(): WifiConfiguration {
|
||||
val configs = runAsShell(permission.NETWORK_SETTINGS) {
|
||||
wifiManager.getConfiguredNetworks()
|
||||
}
|
||||
// If no network is configured, add a config for virtual access points if applicable
|
||||
if (configs.size == 0) {
|
||||
val scanResults = getWifiScanResults()
|
||||
val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
|
||||
assertNotNull(virtualConfig, "The device has no configured wifi network")
|
||||
return virtualConfig
|
||||
}
|
||||
// No need to add a configuration: there is already one.
|
||||
if (configs.size > 1) {
|
||||
// For convenience in case of local testing on devices with multiple saved configs,
|
||||
// prefer the first configuration that is in range.
|
||||
// In actual tests, there should only be one configuration, and it should be usable as
|
||||
// assumed by WifiManagerTest.testConnect.
|
||||
Log.w(TAG, "Multiple wifi configurations found: " +
|
||||
configs.joinToString(", ") { it.SSID })
|
||||
val scanResultsList = getWifiScanResults()
|
||||
Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
|
||||
"${it.SSID} (${it.level})"
|
||||
})
|
||||
|
||||
val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
|
||||
return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
|
||||
}
|
||||
return configs[0]
|
||||
}
|
||||
|
||||
private fun getWifiScanResults(): List<ScanResult> {
|
||||
val scanResultsFuture = CompletableFuture<List<ScanResult>>()
|
||||
runAsShell(permission.NETWORK_SETTINGS) {
|
||||
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
scanResultsFuture.complete(wifiManager.scanResults)
|
||||
}
|
||||
}
|
||||
context.registerReceiver(receiver,
|
||||
IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
|
||||
wifiManager.startScan()
|
||||
}
|
||||
return try {
|
||||
scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
throw AssertionError("Wifi scan results not received within timeout", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a virtual wifi network is detected, add a configuration for that network.
|
||||
* TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
|
||||
*/
|
||||
private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
|
||||
// Virtual wifi networks used on the emulator and cloud testing infrastructure
|
||||
val virtualSsids = listOf("VirtWifi", "AndroidWifi")
|
||||
Log.d(TAG, "Wifi scan results: $scanResults")
|
||||
val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
|
||||
?: return null
|
||||
|
||||
// Only add the virtual configuration if the virtual AP is detected in scans
|
||||
val virtualConfig = WifiConfiguration()
|
||||
// ASCII SSIDs need to be surrounded by double quotes
|
||||
virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
|
||||
virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
runAsShell(permission.NETWORK_SETTINGS) {
|
||||
val networkId = wifiManager.addNetwork(virtualConfig)
|
||||
assertTrue(networkId >= 0)
|
||||
assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
|
||||
}
|
||||
return virtualConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enable wifi networks that were blocked, typically because no internet connection was
|
||||
* detected the last time they were connected. This is necessary to make sure wifi can reconnect
|
||||
* to them.
|
||||
*/
|
||||
private fun clearWifiBlocklist() {
|
||||
runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
|
||||
for (cfg in wifiManager.configuredNetworks) {
|
||||
assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.testutils
|
||||
|
||||
import com.android.ddmlib.testrunner.TestResult
|
||||
import com.android.tradefed.invoker.TestInformation
|
||||
import com.android.tradefed.result.CollectingTestListener
|
||||
import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
|
||||
import com.android.tradefed.targetprep.BaseTargetPreparer
|
||||
import com.android.tradefed.targetprep.suite.SuiteApkInstaller
|
||||
|
||||
private const val CONNECTIVITY_CHECKER_APK = "ConnectivityChecker.apk"
|
||||
private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitychecker"
|
||||
// As per the <instrumentation> defined in the checker manifest
|
||||
private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
/**
|
||||
* A target preparer that verifies that the device was setup correctly for connectivity tests.
|
||||
*
|
||||
* For quick and dirty local testing, can be disabled by running tests with
|
||||
* "atest -- --test-arg com.android.testutils.ConnectivityCheckTargetPreparer:disable:true".
|
||||
*/
|
||||
class ConnectivityCheckTargetPreparer : BaseTargetPreparer() {
|
||||
val installer = SuiteApkInstaller()
|
||||
|
||||
override fun setUp(testInformation: TestInformation) {
|
||||
if (isDisabled) return
|
||||
installer.setCleanApk(true)
|
||||
installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
|
||||
installer.setShouldGrantPermission(true)
|
||||
installer.setUp(testInformation)
|
||||
|
||||
val runner = DefaultRemoteAndroidTestRunner(
|
||||
CONNECTIVITY_PKG_NAME,
|
||||
CONNECTIVITY_CHECK_RUNNER_NAME,
|
||||
testInformation.device.iDevice)
|
||||
runner.runOptions = "--no-hidden-api-checks"
|
||||
|
||||
val receiver = CollectingTestListener()
|
||||
if (!testInformation.device.runInstrumentationTests(runner, receiver)) {
|
||||
throw AssertionError("Device state check failed to complete")
|
||||
}
|
||||
|
||||
val runResult = receiver.currentRunResults
|
||||
if (runResult.isRunFailure) {
|
||||
throw AssertionError("Failed to check device state before the test: " +
|
||||
runResult.runFailureMessage)
|
||||
}
|
||||
|
||||
if (!runResult.hasFailedTests()) return
|
||||
val errorMsg = runResult.testResults.mapNotNull { (testDescription, testResult) ->
|
||||
if (TestResult.TestStatus.FAILURE != testResult.status) null
|
||||
else "$testDescription: ${testResult.stackTrace}"
|
||||
}.joinToString("\n")
|
||||
|
||||
throw AssertionError("Device setup checks failed. Check the test bench: \n$errorMsg")
|
||||
}
|
||||
|
||||
override fun tearDown(testInformation: TestInformation?, e: Throwable?) {
|
||||
if (isTearDownDisabled) return
|
||||
installer.tearDown(testInformation, e)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue