diff options
author | Remi NGUYEN VAN <reminv@google.com> | 2021-11-05 04:44:35 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-11-05 04:44:35 +0000 |
commit | 37e0cdf61042a0599eb8840bcac75b0375534dd9 (patch) | |
tree | ca61adfcb81e2fe22f288c7cdfc0b0ce47fbf400 | |
parent | 610f38422d60a57d7a7af895036ab1db63ca536b (diff) | |
parent | 9e57180bd695a5577e1d90a84a9f849d5b6d4128 (diff) | |
download | net-37e0cdf61042a0599eb8840bcac75b0375534dd9.tar.gz |
Merge "Add ConnectivityCheckTargetPreparer" am: 9e57180bd6
Original change: https://android-review.googlesource.com/c/platform/frameworks/libs/net/+/1842719
Change-Id: I19ee013dcb06f2d1b65342638ef5b74d5f2cb08e
6 files changed, 417 insertions, 0 deletions
diff --git a/common/testutils/Android.bp b/common/testutils/Android.bp index b7297bb8..9fd30f70 100644 --- a/common/testutils/Android.bp +++ b/common/testutils/Android.bp @@ -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"], +} diff --git a/common/testutils/app/connectivitychecker/Android.bp b/common/testutils/app/connectivitychecker/Android.bp new file mode 100644 index 00000000..55b585ae --- /dev/null +++ b/common/testutils/app/connectivitychecker/Android.bp @@ -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"], +}
\ No newline at end of file diff --git a/common/testutils/app/connectivitychecker/AndroidManifest.xml b/common/testutils/app/connectivitychecker/AndroidManifest.xml new file mode 100644 index 00000000..8e5958c3 --- /dev/null +++ b/common/testutils/app/connectivitychecker/AndroidManifest.xml @@ -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> diff --git a/common/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt b/common/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt new file mode 100644 index 00000000..43b130b2 --- /dev/null +++ b/common/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt @@ -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) + } +}
\ No newline at end of file diff --git a/common/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/common/testutils/devicetests/com/android/testutils/ConnectUtil.kt new file mode 100644 index 00000000..fc951d86 --- /dev/null +++ b/common/testutils/devicetests/com/android/testutils/ConnectUtil.kt @@ -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 */)) + } + } + } +}
\ No newline at end of file diff --git a/common/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt b/common/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt new file mode 100644 index 00000000..85589ad9 --- /dev/null +++ b/common/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt @@ -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) + } +}
\ No newline at end of file |