/* * 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).") } } }