blob: 970bb42ed62a539502a8e737937996c4a158f142 [file] [log] [blame]
/*
* Copyright (C) 2022 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.idea.instrumentation.threading
import com.android.testutils.MockitoKt.eq
import com.android.testutils.MockitoKt.mock
import com.android.testutils.VirtualTimeScheduler
import com.android.tools.analytics.TestUsageTracker
import com.android.tools.analytics.UsageTracker
import com.android.tools.instrumentation.threading.agent.callback.ThreadingCheckerTrampoline
import com.google.common.truth.Truth
import com.google.wireless.android.sdk.stats.AndroidStudioEvent
import com.google.wireless.android.sdk.stats.ThreadingAgentUsageEvent
import com.intellij.testFramework.EdtRule
import com.intellij.testFramework.RunsInEdt
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import kotlin.concurrent.thread
@RunsInEdt
class ThreadingCheckerHookImplTest {
@get:Rule
var edtRule = EdtRule()
@get:Rule
val exceptionRule = ExpectedException.none()
private val mockThreadingViolationNotifier = mock<ThreadingViolationNotifier>()
private val threadingCheckerHook = ThreadingCheckerHookImpl(mockThreadingViolationNotifier)
private val tracker = TestUsageTracker(VirtualTimeScheduler())
@Before
fun setUp() {
UsageTracker.setWriterForTest(tracker)
}
@After
fun cleanUp() {
UsageTracker.cleanAfterTesting()
}
init {
ThreadingCheckerTrampoline.installHook(threadingCheckerHook)
// Do not log errors as to not throw exceptions by default
System.setProperty("android.studio.instrumentation.threading.log-errors", "false")
}
@Test
fun testVerifyOnUiThread_addsViolation_whenCalledFromWorkerThread() {
System.setProperty("android.studio.instrumentation.threading.suppress-notifications", "false")
val expectedViolatingMethod =
"com.android.tools.idea.instrumentation.threading.ThreadingCheckerHookImplTest\$checkForUiThreadOnWorkerThread\$1#invoke"
checkForUiThreadOnWorkerThread()
Truth.assertThat(threadingCheckerHook.threadingViolations.keys).containsExactly(expectedViolatingMethod)
Truth.assertThat(threadingCheckerHook.threadingViolations[expectedViolatingMethod]!!.get()).isEqualTo(1L)
verify(mockThreadingViolationNotifier).notify(
eq("Threading violation: methods annotated with @UiThread should be called on the UI thread"),
eq(expectedViolatingMethod))
checkForUiThreadOnWorkerThread()
Truth.assertThat(threadingCheckerHook.threadingViolations.keys).containsExactly(expectedViolatingMethod)
Truth.assertThat(threadingCheckerHook.threadingViolations[expectedViolatingMethod]!!.get()).isEqualTo(2L)
verifyNoMoreInteractions(mockThreadingViolationNotifier)
Truth.assertThat(tracker.usages.map { u -> u.studioEvent.toBuilder().clearStudioSessionId().clearIdeBrand().build() })
.containsExactly(AndroidStudioEvent.newBuilder()
.setKind(AndroidStudioEvent.EventKind.THREADING_AGENT_STATS)
.setThreadingAgentUsageEvent(
ThreadingAgentUsageEvent.newBuilder()
.setVerifyUiThreadCount(1) // The value is 1 and not 2 since we limit the frequency of logged events
.setVerifyWorkerThreadCount(0))
.build())
}
private fun checkForUiThreadOnWorkerThread() {
thread {
ThreadingCheckerTrampoline.verifyOnUiThread()
}.join()
}
@Test
fun testVerifyOnWorkerThread_addsViolation_whenCalledFromUiThread() {
System.setProperty("android.studio.instrumentation.threading.suppress-notifications", "false")
val expectedViolatingMethod =
"com.android.tools.idea.instrumentation.threading.ThreadingCheckerHookImplTest#testVerifyOnWorkerThread_addsViolation_whenCalledFromUiThread"
ThreadingCheckerTrampoline.verifyOnWorkerThread()
Truth.assertThat(threadingCheckerHook.threadingViolations.keys).containsExactly(expectedViolatingMethod)
Truth.assertThat(threadingCheckerHook.threadingViolations[expectedViolatingMethod]!!.get()).isEqualTo(1L)
verify(mockThreadingViolationNotifier).notify(
eq("Threading violation: methods annotated with @WorkerThread should not be called on the UI thread"),
eq(expectedViolatingMethod))
ThreadingCheckerTrampoline.verifyOnWorkerThread()
Truth.assertThat(threadingCheckerHook.threadingViolations.keys).containsExactly(expectedViolatingMethod)
Truth.assertThat(threadingCheckerHook.threadingViolations[expectedViolatingMethod]!!.get()).isEqualTo(2L)
verifyNoMoreInteractions(mockThreadingViolationNotifier)
Truth.assertThat(tracker.usages.map { u -> u.studioEvent.toBuilder().clearStudioSessionId().clearIdeBrand().build() })
.containsExactly(AndroidStudioEvent.newBuilder()
.setKind(AndroidStudioEvent.EventKind.THREADING_AGENT_STATS)
.setThreadingAgentUsageEvent(
ThreadingAgentUsageEvent.newBuilder()
.setVerifyUiThreadCount(0)
.setVerifyWorkerThreadCount(1)) // The value is 1 and not 2 since we limit the frequency of logged events
.build())
}
@Test
fun testVerifyOnUiThread_doesNotAddViolation_whenCalledFromUiThread() {
ThreadingCheckerTrampoline.verifyOnUiThread()
Truth.assertThat(threadingCheckerHook.threadingViolations.keys).isEmpty()
}
@Test
fun testVerifyOnWorkerThread_doesNotAddViolation_whenCalledFromWorkerThread() {
thread {
ThreadingCheckerTrampoline.verifyOnWorkerThread()
}.join()
Truth.assertThat(threadingCheckerHook.threadingViolations.keys).isEmpty()
}
@Test
fun testUsingSystemPropertyToSuppressNotifications() {
val propertyName = "android.studio.instrumentation.threading.suppress-notifications"
val origPropValue = System.getProperty(propertyName)
try {
System.setProperty(propertyName, "true")
ThreadingCheckerTrampoline.verifyOnWorkerThread()
verifyNoMoreInteractions(mockThreadingViolationNotifier)
}
finally {
if (origPropValue != null) {
System.setProperty(propertyName, origPropValue)
}
else {
System.clearProperty(propertyName)
}
}
}
@Test
fun testUsingSystemPropertyToLogErrorsInsteadOfWarnings() {
val propertyName = "android.studio.instrumentation.threading.log-errors"
val origPropValue = System.getProperty(propertyName)
try {
System.setProperty(propertyName, "true")
// Note that logger.error() call inside a unit test results in an exception being thrown
exceptionRule.expect(AssertionError::class.java)
exceptionRule.expectMessage("Threading violation")
ThreadingCheckerTrampoline.verifyOnWorkerThread()
}
finally {
if (origPropValue != null) {
System.setProperty(propertyName, origPropValue)
}
else {
System.clearProperty(propertyName)
}
}
}
}