blob: 701666ca3f4d341a5f6d0b37e367463b822717e1 [file] [log] [blame]
/*
* Copyright (C) 2020 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.testutils
import android.Manifest.permission.MANAGE_TEST_NETWORKS
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
import android.os.Handler
import android.os.HandlerThread
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import kotlin.test.assertFalse
import kotlin.test.fail
private const val HANDLER_TIMEOUT_MS = 10_000L
/**
* A [TestRule] that sets up a [TapPacketReader] on a [TestNetworkInterface] for use in the test.
*
* @param maxPacketSize Maximum size of packets read in the [TapPacketReader] buffer.
* @param autoStart Whether to initialize the interface and start the reader automatically for every
* test. If false, each test must either call start() and stop(), or be annotated
* with TapPacketReaderTest before using the reader or interface.
*/
class TapPacketReaderRule @JvmOverloads constructor(
private val maxPacketSize: Int = 1500,
private val autoStart: Boolean = true
) : TestRule {
// Use lateinit as the below members can't be initialized in the rule constructor (the
// InstrumentationRegistry may not be ready), but from the point of view of test cases using
// this rule with autoStart = true, the members are always initialized (in setup/test/teardown):
// tests cases should be able use them directly.
// lateinit also allows getting good exceptions detailing what went wrong if the members are
// referenced before they could be initialized (typically if autoStart is false and the test
// does not call start or use @TapPacketReaderTest).
lateinit var iface: TestNetworkInterface
lateinit var reader: TapPacketReader
@Volatile
private var readerRunning = false
/**
* Indicates that the [TapPacketReaderRule] should initialize its [TestNetworkInterface] and
* start the [TapPacketReader] before the test, and tear them down afterwards.
*
* For use when [TapPacketReaderRule] is created with autoStart = false.
*/
annotation class TapPacketReaderTest
/**
* Initialize the tap interface and start the [TapPacketReader].
*
* Tests using this method must also call [stop] before exiting.
* @param handler Handler to run the reader on. Callers are responsible for safely terminating
* the handler when the test ends. If null, a handler thread managed by the
* rule will be used.
*/
@JvmOverloads
fun start(handler: Handler? = null) {
if (this::iface.isInitialized) {
fail("${TapPacketReaderRule::class.java.simpleName} was already started")
}
val ctx = InstrumentationRegistry.getInstrumentation().context
iface = runAsShell(MANAGE_TEST_NETWORKS) {
val tnm = ctx.getSystemService(TestNetworkManager::class.java)
?: fail("Could not obtain the TestNetworkManager")
tnm.createTapInterface()
}
val usedHandler = handler ?: HandlerThread(
TapPacketReaderRule::class.java.simpleName).apply { start() }.threadHandler
reader = TapPacketReader(usedHandler, iface.fileDescriptor.fileDescriptor, maxPacketSize)
reader.startAsyncForTest()
readerRunning = true
}
/**
* Stop the [TapPacketReader].
*
* Tests calling [start] must call this method before exiting. If a handler was specified in
* [start], all messages on that handler must also be processed after calling this method and
* before exiting.
*
* If [start] was not called, calling this method is a no-op.
*/
fun stop() {
// The reader may not be initialized if the test case did not use the rule, even though
// other test cases in the same class may be using it (so test classes may call stop in
// tearDown even if start is not called for all test cases).
if (!this::reader.isInitialized) return
reader.handler.post {
reader.stop()
readerRunning = false
}
}
override fun apply(base: Statement, description: Description): Statement {
return TapReaderStatement(base, description)
}
private inner class TapReaderStatement(
private val base: Statement,
private val description: Description
) : Statement() {
override fun evaluate() {
val shouldStart = autoStart ||
description.getAnnotation(TapPacketReaderTest::class.java) != null
if (shouldStart) {
start()
}
try {
base.evaluate()
} finally {
if (shouldStart) {
stop()
reader.handler.looper.apply {
quitSafely()
thread.join(HANDLER_TIMEOUT_MS)
assertFalse(thread.isAlive,
"HandlerThread did not exit within $HANDLER_TIMEOUT_MS ms")
}
}
if (this@TapPacketReaderRule::iface.isInitialized) {
iface.fileDescriptor.close()
}
}
assertFalse(readerRunning,
"stop() was not called, or the provided handler did not process the stop " +
"message before the test ended. If not using autostart, make sure to call " +
"stop() after the test. If a handler is specified in start(), make sure all " +
"messages are processed after calling stop(), before quitting (for example " +
"by using HandlerThread#quitSafely and HandlerThread#join).")
}
}
}