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:
@@ -19,6 +19,7 @@ package com.android.testutils
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.reflect.Modifier
|
||||
import org.junit.runner.Description
|
||||
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.Sortable
|
||||
import org.junit.runner.manipulation.Sorter
|
||||
import org.junit.runner.notification.Failure
|
||||
import org.junit.runner.notification.RunNotifier
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@@ -52,6 +54,9 @@ import org.junit.runners.Parameterized
|
||||
* class MyTestClass { ... }
|
||||
*/
|
||||
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
|
||||
// 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) :
|
||||
@@ -61,6 +66,10 @@ class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, So
|
||||
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 {
|
||||
val ignoreAfter = it.getAnnotation(IgnoreAfter::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) }
|
||||
|
||||
override fun run(notifier: RunNotifier) {
|
||||
if (baseRunner != null) {
|
||||
if (baseRunner == null) {
|
||||
// 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
|
||||
// would see a NoTestsRemainException and not call the run method.
|
||||
notifier.fireTestIgnored(
|
||||
Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
|
||||
return
|
||||
}
|
||||
if (!shouldThreadLeakFailTest) {
|
||||
baseRunner.run(notifier)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// would see a NoTestsRemainException and not call the run method.
|
||||
notifier.fireTestIgnored(
|
||||
Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
|
||||
// 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 {
|
||||
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 {
|
||||
// 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)
|
||||
|
||||
@@ -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
|
||||
* still uses the Easymock structure, which could be simplified.
|
||||
*/
|
||||
@DevSdkIgnoreRunner.MonitorThreadLeak
|
||||
@RunWith(DevSdkIgnoreRunner.class)
|
||||
@SmallTest
|
||||
// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
|
||||
|
||||
Reference in New Issue
Block a user