blob: 44b29f586b705d08adcaa4fdf9054669aa16aec4 [file] [log] [blame]
/*
* 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.tools.appinspection.network
import android.os.Build
import androidx.inspection.ArtTooling
import androidx.inspection.InspectorEnvironment
import androidx.inspection.InspectorExecutors
import com.android.tools.appinspection.common.testing.FakeArtTooling
import com.android.tools.appinspection.common.testing.LogPrinterRule
import com.android.tools.appinspection.network.testing.FakeConnection
import com.android.tools.appinspection.network.testing.FakeEnvironment
import com.android.tools.appinspection.network.testing.FakeTrafficStatsProvider.Stat
import com.android.tools.appinspection.network.testing.NetworkInspectorRule
import com.android.tools.appinspection.network.testing.getLogLines
import com.android.tools.appinspection.network.testing.getVisibleLogLines
import com.google.common.truth.Truth.assertThat
import io.grpc.ClientInterceptor
import io.grpc.ManagedChannelBuilder
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.mockito.Mockito.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.junit.rules.CloseGuardRule
import studio.network.inspection.NetworkInspectorProtocol.SpeedEvent
@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
minSdk = Build.VERSION_CODES.O,
maxSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
)
internal class NetworkInspectorTest {
private val inspectorRule = NetworkInspectorRule(autoStart = false)
@get:Rule
val rule: RuleChain =
RuleChain.outerRule(CloseGuardRule()).around(inspectorRule).around(LogPrinterRule())
private val trafficStatsProvider
get() = inspectorRule.trafficStatsProvider
@Test
fun speedDataCollection() = runBlocking {
trafficStatsProvider.setData(Stat(0, 0), Stat(10, 10), Stat(20, 10), Stat(20, 20), Stat(20, 30))
inspectorRule.start()
delay(1000)
assertThat(inspectorRule.connection.speedData.map { it.toDebugString() })
.containsExactly(
speedEvent(20, 20),
speedEvent(20, 0),
speedEvent(0, 20),
speedEvent(0, 20),
speedEvent(0, 0),
)
.inOrder()
}
@Test
fun speedDataCollection_omitsZeroEvents() = runBlocking {
trafficStatsProvider.setData(
Stat(0, 0),
Stat(10, 10),
Stat(10, 10),
Stat(10, 10),
Stat(10, 10),
Stat(20, 20),
Stat(20, 20),
Stat(20, 20),
Stat(20, 20),
Stat(20, 20),
Stat(20, 20),
Stat(30, 30),
Stat(30, 30),
Stat(30, 30),
Stat(30, 30),
Stat(30, 30),
Stat(30, 30),
)
val response = inspectorRule.start()
delay(1000)
assertThat(response.startInspectionResponse.speedCollectionStarted).isTrue()
assertThat(inspectorRule.connection.speedData.map { it.toDebugString() })
.containsExactly(
speedEvent(20, 20),
speedEvent(0, 0),
speedEvent(0, 0),
speedEvent(20, 20),
speedEvent(0, 0),
speedEvent(0, 0),
speedEvent(20, 20),
speedEvent(0, 0),
)
.inOrder()
}
@Test
fun registerHooks_logs() {
val networkInspector = NetworkInspector(FakeConnection(), FakeEnvironment())
val (javaNet, okhttp, grpc) = networkInspector.registerHooks()
// Note that `AndroidChannelBuilder` and `OkHttpChannelBuilder` are not hooked. This is because
// we didn't add a build dependency on `grpc-android` and `grpc-okhttp`.
// The test still verifies that we attempt to hook it.
assertThat(getLogLines())
.containsExactly(
"DEBUG: studio.inspectors: Instrumented java.net.URL",
"DEBUG: studio.inspectors: Instrumented com.squareup.okhttp.OkHttpClient",
"DEBUG: studio.inspectors: Instrumented okhttp3.OkHttpClient",
"DEBUG: studio.inspectors: Instrumented io.grpc.ManagedChannelBuilder#forAddress",
"DEBUG: studio.inspectors: Instrumented io.grpc.ManagedChannelBuilder#forTarget",
"DEBUG: studio.inspectors: Instrumented io.grpc.okhttp.OkHttpChannelBuilder#forAddress",
"DEBUG: studio.inspectors: Instrumented io.grpc.okhttp.OkHttpChannelBuilder#forTarget",
"DEBUG: studio.inspectors: Could not load class io.grpc.android.AndroidChannelBuilder#forAddress",
"DEBUG: studio.inspectors: Could not load class io.grpc.android.AndroidChannelBuilder#forTarget",
)
assertThat(javaNet).isTrue()
assertThat(okhttp).isTrue()
assertThat(grpc).isTrue()
}
@Test
fun registerHooks_failToAddOkHttp2And3Hooks_doesNotThrowException() {
val environment = TestInspectorEnvironment("OkHttpClient")
val networkInspector = NetworkInspector(FakeConnection(), environment)
val (javaNet, okhttp, grpc) = networkInspector.registerHooks()
assertThat(getVisibleLogLines())
.containsExactly(
"DEBUG: Network Inspector: Did not instrument OkHttpClient. App does not use OKHttp or class is omitted by app reduce"
)
assertThat(javaNet).isTrue()
assertThat(okhttp).isFalse()
assertThat(grpc).isTrue()
}
@Test
fun registerHooks_failToAddOkHttp2_doesNotThrowExceptionDoesNotLogFailure() {
val environment = TestInspectorEnvironment("com.squareup.okhttp.OkHttpClient")
val networkInspector = NetworkInspector(FakeConnection(), environment)
val (javaNet, okhttp, grpc) = networkInspector.registerHooks()
assertThat(getVisibleLogLines()).isEmpty()
assertThat(javaNet).isTrue()
assertThat(okhttp).isTrue()
assertThat(grpc).isTrue()
}
@Test
fun registerHooks_failToAddOkHttp3_doesNotThrowExceptionDoesNotLogFailure() {
val environment = TestInspectorEnvironment("okhttp3.OkHttpClient")
val networkInspector = NetworkInspector(FakeConnection(), environment)
val (javaNet, okhttp, grpc) = networkInspector.registerHooks()
assertThat(getVisibleLogLines()).isEmpty()
assertThat(javaNet).isTrue()
assertThat(okhttp).isTrue()
assertThat(grpc).isTrue()
}
@Test
fun failToAddGrpcHooks_doesNotThrowException() {
val environment = TestInspectorEnvironment("ManagedChannelBuilder")
val networkInspector = NetworkInspector(FakeConnection(), environment)
val (javaNet, okhttp, grpc) = networkInspector.registerHooks()
assertThat(getVisibleLogLines())
.containsExactly(
"DEBUG: Network Inspector: Did not instrument 'ManagedChannelBuilder'. App does not use gRPC or class is omitted by app reduce"
)
assertThat(javaNet).isTrue()
assertThat(okhttp).isTrue()
assertThat(grpc).isFalse()
}
@Test
fun hookGrpcChannelBuilder_chainedCalls_installOnce() {
inspectorRule.start()
val artTooling = inspectorRule.environment.artTooling() as FakeArtTooling
val mockChannelBuilder = mock<ManagedChannelBuilder<*>>()
// In reality, chained calls will not be recursive, but it's simpler to test with the same class
// and method.
val clazz = ManagedChannelBuilder::class.java
val method = "forTarget(Ljava/lang/String;)Lio/grpc/ManagedChannelBuilder;"
artTooling.triggerEntryHook(clazz, method, null, emptyList())
artTooling.triggerEntryHook(clazz, method, null, emptyList())
artTooling.triggerEntryHook(clazz, method, null, emptyList())
artTooling.triggerExitHook(clazz, method, mockChannelBuilder)
artTooling.triggerExitHook(clazz, method, mockChannelBuilder)
artTooling.triggerExitHook(clazz, method, mockChannelBuilder)
verify(mockChannelBuilder, times(1)).intercept(any<ClientInterceptor>())
}
private inner class TestInspectorEnvironment(private val rejectClassName: String) :
InspectorEnvironment {
override fun artTooling(): ArtTooling {
return object : ArtTooling by inspectorRule.environment.artTooling() {
override fun <T : Any?> findInstances(clazz: Class<T>): List<T> {
return when {
clazz.name.endsWith(rejectClassName) -> emptyList()
else -> inspectorRule.environment.artTooling().findInstances(clazz)
}
}
override fun <T : Any?> registerExitHook(
originClass: Class<*>,
originMethod: String,
exitHook: ArtTooling.ExitHook<T>,
) {
if (originClass.name.endsWith(rejectClassName)) {
throw NoClassDefFoundError()
} else {
inspectorRule.environment
.artTooling()
.registerExitHook(originClass, originMethod, exitHook)
}
}
}
}
override fun executors(): InspectorExecutors {
return inspectorRule.environment.executors()
}
}
}
private fun speedEvent(rxSpeed: Long, txSpeed: Long) =
SpeedEvent.newBuilder().setRxSpeed(rxSpeed).setTxSpeed(txSpeed).build().toDebugString()
// Proto.toString is hard to read because fields with default values are elided
private fun SpeedEvent.toDebugString() = "rx=$rxSpeed tx=$txSpeed"