Ensure no thread leak after NetworkStatsServiceTest

This change includes:
 1. A mechanism in DevSdkIngoreRunnrer to dump thread counts
    before/after tests, and compare thread counts to detect leaks.
 2. Add an annotation @MonitorThreadLeak for test classes to
    annotate the classes which should monitor thread leaks.
 3. Annotated NetworkStatsServiceTest to apply the enforcement.

Sample output:
[1/2] android.net.connectivity.com.android.server.net.NetworkStatsServiceTest#testDumpStatsMap: PASSED (1.187s)
[2/2] android.net.connectivity.com.android.server.net.NetworkStatsServiceTest#ThreadLeakMonitor: FAILED (7ms)

STACKTRACE:
java.lang.IllegalStateException: Expected threads: {binder:26055_3=1, Instr: androidx.test.runner.AndroidJUnitRunner=1, main=1, InstrumentationConnectionThread=1, binder:26055_2=1, binder:26055_1=1} but got: {binder:26055_3=1, NetworkStatsObservers=1, Instr: androidx.test.runner.AndroidJUnitRunner=1, main=1, InstrumentationConnectionThread=1, binder:26055_2=1, binder:26055_1=1}

Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.net.NetworkStatsServiceTest \
      --rerun-until-failure 100
Bug: 308544001
Bug: 307693729
Change-Id: Ia0bccb82c5985df608b8402009b32626b6b17c5a
This commit is contained in:
Junyu Lai
2023-11-06 16:21:16 +08:00
parent 8b61783b78
commit 17f589cf47
2 changed files with 52 additions and 8 deletions

View File

@@ -19,6 +19,7 @@ package com.android.testutils
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import java.lang.IllegalStateException
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runner.Runner import org.junit.runner.Runner
@@ -27,6 +28,7 @@ import org.junit.runner.manipulation.Filterable
import org.junit.runner.manipulation.NoTestsRemainException import org.junit.runner.manipulation.NoTestsRemainException
import org.junit.runner.manipulation.Sortable import org.junit.runner.manipulation.Sortable
import org.junit.runner.manipulation.Sorter import org.junit.runner.manipulation.Sorter
import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunNotifier import org.junit.runner.notification.RunNotifier
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
@@ -52,6 +54,9 @@ import org.junit.runners.Parameterized
* class MyTestClass { ... } * class MyTestClass { ... }
*/ */
class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable { class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
// Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
// Java bytecode doesn't have a way to express this. Give this type a name by wrapping it. // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
private class RunnerWrapper<T>(private val wrapped: T) : private class RunnerWrapper<T>(private val wrapped: T) :
@@ -61,6 +66,10 @@ class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, So
override fun run(notifier: RunNotifier?) = wrapped.run(notifier) override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
} }
// Annotation for test classes to indicate the test runner should monitor thread leak.
// TODO(b/307693729): Remove this annotation and monitor thread leak by default.
annotation class MonitorThreadLeak
private val baseRunner: RunnerWrapper<*>? = klass.let { private val baseRunner: RunnerWrapper<*>? = klass.let {
val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java) val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java) val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -81,20 +90,52 @@ class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, So
it.isAnnotationPresent(Parameterized.Parameters::class.java) } it.isAnnotationPresent(Parameterized.Parameters::class.java) }
override fun run(notifier: RunNotifier) { override fun run(notifier: RunNotifier) {
if (baseRunner != null) { if (baseRunner == null) {
baseRunner.run(notifier)
return
}
// Report a single, skipped placeholder test for this class, as the class is expected to // Report a single, skipped placeholder test for this class, as the class is expected to
// report results when run. In practice runners that apply the Filterable implementation // report results when run. In practice runners that apply the Filterable implementation
// would see a NoTestsRemainException and not call the run method. // would see a NoTestsRemainException and not call the run method.
notifier.fireTestIgnored( notifier.fireTestIgnored(
Description.createTestDescription(klass, "skippedClassForDevSdkMismatch")) Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
return
}
if (!shouldThreadLeakFailTest) {
baseRunner.run(notifier)
return
}
// Dump threads as a baseline to monitor thread leaks.
val threadCountsBeforeTest = getAllThreadNameCounts()
baseRunner.run(notifier)
notifier.fireTestStarted(leakMonitorDesc)
val threadCountsAfterTest = getAllThreadNameCounts()
if (threadCountsBeforeTest != threadCountsAfterTest) {
notifier.fireTestFailure(Failure(leakMonitorDesc,
IllegalStateException("Expected threads: $threadCountsBeforeTest " +
"but got: $threadCountsAfterTest")))
}
notifier.fireTestFinished(leakMonitorDesc)
}
private fun getAllThreadNameCounts(): Map<String, Int> {
// Get the counts of threads in the group per name.
// Filter system thread groups.
return Thread.getAllStackTraces().keys
.filter { it.threadGroup?.name != "system" }
.groupingBy { it.name }.eachCount()
} }
override fun getDescription(): Description { override fun getDescription(): Description {
return baseRunner?.description ?: Description.createSuiteDescription(klass) if (baseRunner == null) {
return Description.createSuiteDescription(klass)
}
return baseRunner.description.also {
if (shouldThreadLeakFailTest) {
it.addChild(leakMonitorDesc)
}
}
} }
/** /**
@@ -102,7 +143,9 @@ class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, So
*/ */
override fun testCount(): Int { override fun testCount(): Int {
// When ignoring the tests, a skipped placeholder test is reported, so test count is 1. // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
return baseRunner?.testCount() ?: 1 if (baseRunner == null) return 1
return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
} }
@Throws(NoTestsRemainException::class) @Throws(NoTestsRemainException::class)

View File

@@ -193,6 +193,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
* still uses the Easymock structure, which could be simplified. * still uses the Easymock structure, which could be simplified.
*/ */
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class) @RunWith(DevSdkIgnoreRunner.class)
@SmallTest @SmallTest
// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible // NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible