summaryrefslogtreecommitdiff
path: root/common/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
blob: 701666ca3f4d341a5f6d0b37e367463b822717e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/*
 * 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).")
        }
    }
}