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 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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user