Make a base class for testable callbacks

Have TestableNetworkCallback adopt it, as a first client. It's also by
far the most complex.

Test: TH
Flag: EXEMPT test-only
Change-Id: Ie58d051b4478c901b7f0f54e89a67d22f1ee2ce2
This commit is contained in:
Chalard Jean 2025-05-02 14:51:22 +09:00
commit 2db96977db
9 changed files with 577 additions and 166 deletions

View file

@ -122,6 +122,7 @@ class TestableNetworkCallbackTest {
mCallback.assertNoCallback(SHORT_TIMEOUT_MS)
mCallback.onAvailable(Network(100))
assertFails { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) }
mCallback.expect<Available>()
val net = Network(101)
mCallback.assertNoCallback { it is Available }
mCallback.onAvailable(net)
@ -277,33 +278,33 @@ class TestableNetworkCallbackTest {
fun testAllExpectOverloads() {
// This test should never run, it only checks that all overloads exist and build
assumeTrue(false)
val hn = object : TestableNetworkCallback.HasNetwork { override val network = ANY_NETWORK }
val hn = object : TestableNetworkCallback.HasNetwork { override val network = AnyNetwork }
// Method with all arguments (version that takes a Network)
mCallback.expect(AVAILABLE, ANY_NETWORK, 10, "error") { true }
mCallback.expect(AVAILABLE, AnyNetwork, 10, "error") { true }
// Java overloads omitting one argument. One line for omitting each argument, in positional
// order. Versions that take a Network.
mCallback.expect(AVAILABLE, 10, "error") { true }
mCallback.expect(AVAILABLE, ANY_NETWORK, "error") { true }
mCallback.expect(AVAILABLE, ANY_NETWORK, 10) { true }
mCallback.expect(AVAILABLE, ANY_NETWORK, 10, "error")
mCallback.expect(AVAILABLE, AnyNetwork, "error") { true }
mCallback.expect(AVAILABLE, AnyNetwork, 10) { true }
mCallback.expect(AVAILABLE, AnyNetwork, 10, "error")
// Java overloads for omitting two arguments. One line for omitting each pair of arguments.
// Versions that take a Network.
mCallback.expect(AVAILABLE, "error") { true }
mCallback.expect(AVAILABLE, 10) { true }
mCallback.expect(AVAILABLE, 10, "error")
mCallback.expect(AVAILABLE, ANY_NETWORK) { true }
mCallback.expect(AVAILABLE, ANY_NETWORK, "error")
mCallback.expect(AVAILABLE, ANY_NETWORK, 10)
mCallback.expect(AVAILABLE, AnyNetwork) { true }
mCallback.expect(AVAILABLE, AnyNetwork, "error")
mCallback.expect(AVAILABLE, AnyNetwork, 10)
// Java overloads for omitting three arguments. One line for each remaining argument.
// Versions that take a Network.
mCallback.expect(AVAILABLE) { true }
mCallback.expect(AVAILABLE, "error")
mCallback.expect(AVAILABLE, 10)
mCallback.expect(AVAILABLE, ANY_NETWORK)
mCallback.expect(AVAILABLE, AnyNetwork)
// Java overload for omitting all four arguments.
mCallback.expect(AVAILABLE)
@ -322,23 +323,23 @@ class TestableNetworkCallbackTest {
mCallback.expect(AVAILABLE, hn)
// Same as above but for reified versions.
mCallback.expect<Available>(ANY_NETWORK, 10, "error") { true }
mCallback.expect<Available>(AnyNetwork, 10, "error") { true }
mCallback.expect<Available>(timeoutMs = 10, errorMsg = "error") { true }
mCallback.expect<Available>(network = ANY_NETWORK, errorMsg = "error") { true }
mCallback.expect<Available>(network = ANY_NETWORK, timeoutMs = 10) { true }
mCallback.expect<Available>(network = ANY_NETWORK, timeoutMs = 10, errorMsg = "error")
mCallback.expect<Available>(network = AnyNetwork, errorMsg = "error") { true }
mCallback.expect<Available>(network = AnyNetwork, timeoutMs = 10) { true }
mCallback.expect<Available>(network = AnyNetwork, timeoutMs = 10, errorMsg = "error")
mCallback.expect<Available>(errorMsg = "error") { true }
mCallback.expect<Available>(timeoutMs = 10) { true }
mCallback.expect<Available>(timeoutMs = 10, errorMsg = "error")
mCallback.expect<Available>(network = ANY_NETWORK) { true }
mCallback.expect<Available>(network = ANY_NETWORK, errorMsg = "error")
mCallback.expect<Available>(network = ANY_NETWORK, timeoutMs = 10)
mCallback.expect<Available>(network = AnyNetwork) { true }
mCallback.expect<Available>(network = AnyNetwork, errorMsg = "error")
mCallback.expect<Available>(network = AnyNetwork, timeoutMs = 10)
mCallback.expect<Available> { true }
mCallback.expect<Available>(errorMsg = "error")
mCallback.expect<Available>(timeoutMs = 10)
mCallback.expect<Available>(network = ANY_NETWORK)
mCallback.expect<Available>(network = AnyNetwork)
mCallback.expect<Available>()
mCallback.expect<Available>(hn, 10, "error") { true }

View file

@ -17,7 +17,7 @@
package com.android.testutils;
import static com.android.testutils.TestableNetworkCallback.Event.AVAILABLE;
import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
import static com.android.testutils.TestableNetworkCallbackKt.AnyNetwork;
import static org.junit.Assume.assumeTrue;
@ -25,37 +25,37 @@ import org.junit.Test;
public class TestableNetworkCallbackTestJava {
@Test
void testAllExpectOverloads() {
public void testAllExpectOverloads() {
// This test should never run, it only checks that all overloads exist and build
assumeTrue(false);
final TestableNetworkCallback callback = new TestableNetworkCallback();
TestableNetworkCallback.HasNetwork hn = TestableNetworkCallbackKt::anyNetwork;
TestableNetworkCallback.HasNetwork hn = TestableNetworkCallbackKt::AnyNetwork;
// Method with all arguments (version that takes a Network)
callback.expect(AVAILABLE, anyNetwork(), 10, "error", cb -> true);
callback.expect(AVAILABLE, AnyNetwork(), 10, "error", cb -> true);
// Overloads omitting one argument. One line for omitting each argument, in positional
// order. Versions that take a Network.
callback.expect(AVAILABLE, 10, "error", cb -> true);
callback.expect(AVAILABLE, anyNetwork(), "error", cb -> true);
callback.expect(AVAILABLE, anyNetwork(), 10, cb -> true);
callback.expect(AVAILABLE, anyNetwork(), 10, "error");
callback.expect(AVAILABLE, AnyNetwork(), "error", cb -> true);
callback.expect(AVAILABLE, AnyNetwork(), 10, cb -> true);
callback.expect(AVAILABLE, AnyNetwork(), 10, "error");
// Overloads for omitting two arguments. One line for omitting each pair of arguments.
// Versions that take a Network.
callback.expect(AVAILABLE, "error", cb -> true);
callback.expect(AVAILABLE, 10, cb -> true);
callback.expect(AVAILABLE, 10, "error");
callback.expect(AVAILABLE, anyNetwork(), cb -> true);
callback.expect(AVAILABLE, anyNetwork(), "error");
callback.expect(AVAILABLE, anyNetwork(), 10);
callback.expect(AVAILABLE, AnyNetwork(), cb -> true);
callback.expect(AVAILABLE, AnyNetwork(), "error");
callback.expect(AVAILABLE, AnyNetwork(), 10);
// Overloads for omitting three arguments. One line for each remaining argument.
// Versions that take a Network.
callback.expect(AVAILABLE, cb -> true);
callback.expect(AVAILABLE, "error");
callback.expect(AVAILABLE, 10);
callback.expect(AVAILABLE, anyNetwork());
callback.expect(AVAILABLE, AnyNetwork());
// Java overload for omitting all four arguments.
callback.expect(AVAILABLE);

View file

@ -81,7 +81,7 @@ class ConnectivityCheckTest {
.addCapability(NET_CAPABILITY_INTERNET)
.build()
)
val capChanged = cb.eventuallyExpect<CapabilitiesChanged>(from = 0)
val capChanged = cb.eventuallyExpect<CapabilitiesChanged>()
val network = capChanged.network
val ssid = capChanged.caps.ssid
assertFalse(ssid.isNullOrEmpty(), "No SSID for wifi network $network")

View file

@ -23,7 +23,12 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.util.Log
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.Expectable
import com.android.net.module.util.TestableCallback
import com.android.net.module.util.assertNo
import com.android.net.module.util.eventuallyExpect
import com.android.net.module.util.expect
import com.android.testutils.TestableNetworkCallback.Event
import com.android.testutils.TestableNetworkCallback.Event.Available
import com.android.testutils.TestableNetworkCallback.Event.BlockedStatus
import com.android.testutils.TestableNetworkCallback.Event.BlockedStatusInt
@ -38,12 +43,11 @@ import com.android.testutils.TestableNetworkCallback.Event.Suspended
import com.android.testutils.TestableNetworkCallback.Event.Unavailable
import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.fail
object NULL_NETWORK : Network(-1)
object ANY_NETWORK : Network(-2)
fun anyNetwork() = ANY_NETWORK
object NullNetwork : Network(-1)
object AnyNetwork : Network(-2)
fun AnyNetwork() = AnyNetwork
private val DEFAULT_TAG = TestableNetworkCallback::class.simpleName
?: fail("Could not determine class name")
@ -56,15 +60,15 @@ private val NOOP = Runnable {}
* See comments on the public constructor below for a description of the arguments.
*/
open class TestableNetworkCallback private constructor(
src: TestableNetworkCallback?,
val defaultTimeoutMs: Long,
private val defaultNoCallbackTimeoutMs: Long,
defaultCallbackTimeoutMs: Long,
defaultCallbackNoEventTimeoutMs: Long,
private val waiterFunc: Runnable,
private val logTag: String
) : NetworkCallback() {
private val backingRecord: ArrayTrackRecord<Event> =
src?.backingRecord ?: ArrayTrackRecord()
val history: ArrayTrackRecord<Event>.ReadHead = backingRecord.newReadHead()
private val logTag: String,
val callbackTracker: TestableCallback<Event> = TestableCallback(
defaultCallbackTimeoutMs,
defaultCallbackNoEventTimeoutMs
)
) : NetworkCallback(), Expectable<Event> by callbackTracker {
val mark get() = history.mark
sealed class Event {
@ -80,7 +84,7 @@ open class TestableNetworkCallback private constructor(
override val network: Network,
val caps: NetworkCapabilities
) : Event() {
constructor(caps: NetworkCapabilities) : this(NULL_NETWORK, caps)
constructor(caps: NetworkCapabilities) : this(NullNetwork, caps)
}
data class Available(override val network: Network) : Event()
data class CapabilitiesChanged(
@ -102,7 +106,7 @@ open class TestableNetworkCallback private constructor(
data class Unavailable(
override val network: Network
) : Event() {
constructor() : this(NULL_NETWORK)
constructor() : this(NullNetwork)
}
data class BlockedStatus(
override val network: Network,
@ -226,18 +230,25 @@ open class TestableNetworkCallback private constructor(
noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
waiterFunc: Runnable = NOOP, // "() -> Unit" would forbid calling with a void func from Java
logTag: String = DEFAULT_TAG
) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc, logTag)
) : this(
timeoutMs,
noCallbackTimeoutMs,
waiterFunc,
logTag,
TestableCallback(timeoutMs, noCallbackTimeoutMs)
)
fun createLinkedCopy() = TestableNetworkCallback(
this,
defaultTimeoutMs,
defaultNoCallbackTimeoutMs,
defaultNoEventTimeoutMs,
waiterFunc,
logTag
logTag,
callbackTracker
)
// The last available network, or null if any network was lost since the last call to
// onAvailable. TODO : fix this by fixing the tests that rely on this behavior
// DO NOT ADD CALLS TO THIS METHOD. USE THE RETURN VALUE OF expect<> INSTEAD.
val lastAvailableNetwork: Network?
get() = when (val it = history.lastOrNull { it is Available || it is Lost }) {
is Available -> it.network
@ -254,25 +265,56 @@ open class TestableNetworkCallback private constructor(
fun poll(timeoutMs: Long = defaultTimeoutMs, predicate: (Event) -> Boolean = { true }) =
history.poll(timeoutMs, predicate)
// Proxy methods to avoid having to write expect<_, T>, see TestableCallback.kt
inline fun <reified T : Event> expect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = expect<_, T>(timeoutMs, errorMsg, predicate)
inline fun <reified T : Event> eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = eventuallyExpect<_, T>(timeoutMs, errorMsg, predicate)
inline fun <reified T : Event> assertNo(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = assertNo<_, T>(timeoutMs, errorMsg, predicate)
/*****
* expect family of methods.
* These methods fetch the next callback and assert it matches the conditions : type,
* passed predicate. If no callback is received within the timeout, these methods fail.
* expect family of methods, to support Java code
*
* See Expectable#expect
*/
@Suppress("SuspiciousEqualsCombination") // === and == combination is legit here
@JvmOverloads
fun <T : Event> expect(
type: KClass<T>,
network: Network = ANY_NETWORK,
network: Network = AnyNetwork,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = expect<Event>(network, timeoutMs, errorMsg) {
if (type.isInstance(it)) {
test(it as T) // Cast can't fail since type.isInstance(it) and type: KClass<T>
} else {
fail("Expected callback ${type.simpleName}, got $it")
}
} as T
): T = callbackTracker.expect(type, timeoutMs, errorMsg) {
(network === AnyNetwork || it.network == network) && test(it)
}
// Add a version of the standard expect method that takes a network.
inline fun <reified T : Event> expect(
network: Network = AnyNetwork,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline test: (T) -> Boolean = { true }
) = expect(T::class, network, timeoutMs, errorMsg, test)
inline fun <reified T : Event> expect(
network: HasNetwork,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline test: (T) -> Boolean = { true }
) = expect(T::class, network.network, timeoutMs, errorMsg, test)
@JvmOverloads
fun <T : Event> expect(
@ -287,13 +329,11 @@ open class TestableNetworkCallback private constructor(
// here. Note that @JvmOverloads give us the versions without the last arguments too, so
// there is no need to explicitly define versions without the test predicate.
// Without |network|
@JvmOverloads
fun <T : Event> expect(
type: KClass<T>,
timeoutMs: Long,
errorMsg: String?,
test: (T) -> Boolean = { true }
) = expect(type, ANY_NETWORK, timeoutMs, errorMsg, test)
) = expect(type, AnyNetwork, timeoutMs, errorMsg)
// Without |timeout|, in Network and HasNetwork versions
@JvmOverloads
@ -333,7 +373,7 @@ open class TestableNetworkCallback private constructor(
type: KClass<T>,
errorMsg: String?,
test: (T) -> Boolean = { true }
) = expect(type, ANY_NETWORK, defaultTimeoutMs, errorMsg, test)
) = expect(type, AnyNetwork, defaultTimeoutMs, errorMsg, test)
// Without |network| or |errorMsg|
@JvmOverloads
@ -341,7 +381,7 @@ open class TestableNetworkCallback private constructor(
type: KClass<T>,
timeoutMs: Long,
test: (T) -> Boolean = { true }
) = expect(type, ANY_NETWORK, timeoutMs, null, test)
) = expect(type, AnyNetwork, timeoutMs, null, test)
// Without |timeout| or |errorMsg|, in Network and HasNetwork versions
fun <T : Event> expect(
@ -360,37 +400,54 @@ open class TestableNetworkCallback private constructor(
fun <T : Event> expect(
type: KClass<T>,
test: (T) -> Boolean
) = expect(type, ANY_NETWORK, defaultTimeoutMs, null, test)
) = expect(type, AnyNetwork, defaultTimeoutMs, null, test)
// Kotlin reified versions. Don't call methods above, or the predicate would need to be noinline
inline fun <reified T : Event> expect(
network: Network = ANY_NETWORK,
/*****
* eventuallyExpect family of methods, to support Java code
*
* See Expectable#eventuallyExpect
*/
@JvmOverloads
fun <T : Event> eventuallyExpect(
type: KClass<T>,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = (poll(timeoutMs) ?: failWithErrorReason(errorMsg,
"Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
.also {
if (it !is T) {
failWithErrorReason(
errorMsg,
"Expected callback ${T::class.simpleName}, got $it"
)
}
if (ANY_NETWORK !== network && it.network != network) {
failWithErrorReason(errorMsg, "Expected network $network for callback : $it")
}
if (!test(it)) {
failWithErrorReason(errorMsg, "Callback doesn't match predicate : $it")
}
} as T
predicate: (cb: T) -> Boolean = { true }
) = callbackTracker.eventuallyExpect(type, timeoutMs, errorMsg = null, predicate)
inline fun <reified T : Event> expect(
network: HasNetwork,
// eventuallyExpect with a starting point.
// TODO : remove these methods, their interface is not easy to use ; instead, use an
// explicit out-of-order checking mechanism.
@Suppress("UNCHECKED_CAST")
fun <T : Event> eventuallyExpect(
type: KClass<T>,
timeoutMs: Long,
from: Int,
predicate: (T) -> Boolean
): T {
val event = history.poll(timeoutMs, pos = from) {
type.isInstance(it) && predicate(it as T)
}
if (null == event) {
failWithErrorReason(
null,
"Callback ${type.simpleName} not received within ${timeoutMs}ms. Got\n" +
history.prettyPrintBacktrace(before = 2, after = 2)
)
}
return event as T
}
fun <T : Event> eventuallyExpect(
type: KClass<T>,
from: Int,
predicate: (T) -> Boolean
): T = eventuallyExpect(type, timeoutMs = defaultTimeoutMs, from, predicate)
inline fun <reified T : Event> eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = expect(network.network, timeoutMs, errorMsg, test)
from: Int,
noinline predicate: (T) -> Boolean = { true }
) = eventuallyExpect(T::class, timeoutMs, from, predicate)
/*****
* assertNoCallback family of methods.
@ -400,59 +457,15 @@ open class TestableNetworkCallback private constructor(
*/
@JvmOverloads
fun assertNoCallback(
timeoutMs: Long = defaultNoCallbackTimeoutMs,
timeoutMs: Long = defaultNoEventTimeoutMs,
valid: (Event) -> Boolean = { true }
) {
waiterFunc.run()
history.poll(timeoutMs) { valid(it) }?.let { fail("Expected no callback but got $it") }
callbackTracker.assertNoCallback(timeoutMs, errorMsg = null, valid)
}
fun assertNoCallback(valid: (Event) -> Boolean) =
assertNoCallback(defaultNoCallbackTimeoutMs, valid)
/*****
* eventuallyExpect family of methods.
* These methods make sure a callback that matches the type/predicate is received eventually.
* Any callback of the wrong type, or doesn't match the optional predicate, is ignored.
* They fail if no callback matching the predicate is received within the timeout.
*/
inline fun <reified T : Event> eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
from: Int = mark,
crossinline predicate: (T) -> Boolean = { true }
): T = history.poll(timeoutMs, from) { it is T && predicate(it) }.also {
assertNotNull(
it,
"Callback ${T::class} not received within ${timeoutMs}ms. " +
"Got ${history.backtrace()}"
)
} as T
@JvmOverloads
fun <T : Event> eventuallyExpect(
type: KClass<T>,
timeoutMs: Long = defaultTimeoutMs,
predicate: (cb: T) -> Boolean = { true }
) = history.poll(timeoutMs) { type.java.isInstance(it) && predicate(it as T) }.also {
assertNotNull(
it,
"Callback ${type.java} not received within ${timeoutMs}ms. " +
"Got ${history.backtrace()}"
)
} as T
fun <T : Event> eventuallyExpect(
type: KClass<T>,
timeoutMs: Long = defaultTimeoutMs,
from: Int = mark,
predicate: (cb: T) -> Boolean = { true }
) = history.poll(timeoutMs, from) { type.java.isInstance(it) && predicate(it as T) }.also {
assertNotNull(
it,
"Callback ${type.java} not received within ${timeoutMs}ms. " +
"Got ${history.backtrace()}"
)
} as T
assertNoCallback(defaultNoEventTimeoutMs, valid)
// Expects onAvailable and the callbacks that follow it. These are:
// - onSuspended, iff the network was suspended when the callbacks fire.
@ -639,5 +652,5 @@ open class TestableNetworkCallback private constructor(
fun expectCaps(
tmt: Long,
valid: (NetworkCapabilities) -> Boolean
) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
) = expect<CapabilitiesChanged>(AnyNetwork, tmt) { valid(it.caps) }.caps
}

View file

@ -0,0 +1,358 @@
/*
* Copyright (C) 2025 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.
*/
@file:Suppress("UNCHECKED_CAST") // cast is checked by type.isInstance throughout the file
package com.android.net.module.util
import com.android.testutils.failWithErrorReason
import kotlin.reflect.KClass
private const val DEFAULT_TIMEOUT = 30_000L // ms
private const val DEFAULT_NO_EVENT_TIMEOUT = 200L // ms
/**
* These classes, [Expectable] and [TestableCallback], are tools designed to simplify the testing
* of asynchronous callbacks in your code.
*
* A **callback** is a piece of code that gets executed when a specific event happens (e.g., data
* received, network disconnected). These utilities help you define, record, and then assert the
* occurrence or non-occurrence of these events in your tests.
*
* To use these tools, your test class typically needs to:
* 1. Define a **sealed class** for all possible events that your callback can produce. Each
* specific event should be a `data class` subclass of this sealed event type. This ensures type
* safety and clear event definitions.
* Note : if there is only one event you can skip the sealed class and only have one data class
* representing that event.
* 2. Subclass [TestableCallback] and have your subclass implement the callback interface. If you
* cannot subclass [TestableCallback] because the callback is not an interface, your subclass
* will instead compose a [TestableCallback] which will allow for implementing [Expectable]
* with no additional work.
*
*
* ## How to Use: Two Main Scenarios
*
* ### Scenario 1: Testing an Interface callback (Recommended)
*
* If the callback you are testing is an **interface** (e.g., `NetworkOfferCallback`), then
* your test callback class should **inherit directly from [TestableCallback]**. This gives you
* all the `expect` and `assertNo` family of methods for free.
*
* **Example:** Testing a `TestedServiceCallback` interface with `onFirstAction` and
* `onSecondAction` methods.
* ```kotlin
* // The interface you want to test
* interface TestedServiceCallback {
* fun onFirstAction(data: String)
* fun onSecondAction(value: Int)
* }
*
* class MyTestServiceCallback : TestableCallback<Event>(), TestedServiceCallback {
* // Define all the possible events that can occur from this callback
* sealed class Event {
* data class FirstAction(val data: String) : ServiceEvent()
* data class SecondAction(val value: Int) : ServiceEvent()
* }
*
* // Implement the interface methods and record the events into the history
* override fun onFirstAction(data: String) = history.add(FirstAction(data))
* override fun onSecondAction(value: Int) = history.add(SecondAction(value))
*
* // You can add custom helper methods here if you want more functionality than the default
* // expect/assertNo methods offer.
* }
* ```
*
* **In your test code, you can then:**
* // Wait for a `FirstAction` event where the argument starts with "foo" to happen within the
* // default timeout.
* `myTestServiceCallback.expect<FirstAction>() { it.data.startsWith("foo") }`
* // Wait for any `SecondAction` event for up to 500 milliseconds.
* `myTestServiceCallback.expect<SecondAction>(timeoutMs = 500)`
* // Make sure no `SecondAction` event has fired so far.
* `myTestServiceCallback.assertNo<SecondAction>()
* etc.
*
* ### Scenario 2: Testing a class callback (Only do this when inheritance isn't possible)
*
* If the callback you need to test is a **class** (not an interface), direct inheritance from
* [TestableCallback] isn't possible. In this case, you will:
* 1. **Embed** an instance of [TestableCallback] inside your test class.
* 2. Declare that your test class **implements [Expectable] by delegating** to this embedded
* [TestableCallback] instance. This means your class will use the embedded `TestableCallback`'s
* functionality for event tracking.
* ```kotlin
* MyTestServiceCallback(internalEventTracker: TestableCallback<Event>)
* : TestedServiceCallback, Expectable<Event> by internalEventTracker { ...
* ```
* 3. **Proxy** the `expect`, `eventuallyExpect`, and `assertNo` methods. This makes your test code
* simpler by allowing you to write `expect<MySpecificEvent>()` instead of
* `expect<_, MySpecificEvent>()`.
*
* **Example:** Testing a `TestedServiceCallback` class with `onFirstAction` and `onSecondAction`
* methods.
* ```kotlin
* import com.android.net.module.util.assertNo
* import com.android.net.module.util.expect
* import com.android.net.module.util.eventuallyExpect
*
* // The class you want to test
* open class TestedServiceCallback {
* open fun onFirstAction(data: String)
* open fun onSecondAction(value: Int)
* }
*
* class MyTestServiceCallback(
* // Embed a TestableCallback to handle the actual event tracking
* private val internalEventTracker: TestableCallback<Event> = TestableCallback()
* ) : TestedServiceCallback(), Expectable<Event> by internalEventTracker {
*
* // Define all the possible events that can occur from this callback
* sealed class Event {
* data class FirstAction(val data: String) : ServiceEvent()
* data class SecondAction(val value: Int) : ServiceEvent()
* }
*
* // Implement the interface methods and record the events into the history
* override fun onFirstAction(data: String) = history.add(FirstAction(data))
* override fun onSecondAction(value: Int) = history.add(SecondAction(value))
*
* // Proxy methods: These make it simpler to call expect/assertNo, avoiding 'expect<_, T>'
* // They simply forward the call to the embedded internalEventTracker.
* inline fun <reified T : Event> expect(
* timeoutMs: Long = defaultTimeoutMs,
* errorMsg: String? = null,
* noinline predicate: (T) -> Boolean = { true }
* ) = expect<_, T>(timeoutMs, errorMsg, predicate)
*
* inline fun <reified T : Event> eventuallyExpect(
* timeoutMs: Long = defaultTimeoutMs,
* errorMsg: String? = null,
* noinline predicate: (T) -> Boolean = { true }
* ) = eventuallyExpect<_, T>(timeoutMs, errorMsg, predicate)
*
* inline fun <reified T : Event> assertNo(
* timeoutMs: Long = defaultTimeoutMs,
* errorMsg: String? = null,
* noinline predicate: (T) -> Boolean = { true }
* ) = assertNo<_, T>(timeoutMs, errorMsg, predicate)
* }
* ```
*
*
* ### Explanation of proxy methods (for the curious)
*
* The `expect`, `eventuallyExpect`, and `assertNo` methods typically reside on
* `TestableCallback<Event>`. When you inherit from `TestableCallback`, the type `Event` is
* directly known, making calls like `expect<MySpecificEvent>()` straightforward.
*
* However, `Expectable` is an interface, and as such it cannot have member methods. The
* `Expectable.expect*` family of methods are therefore extension methods with two type arguments :
* one for the type argument of the receiver (`Event`), one for the concrete event type
* (`FirstAction`) ; the former is required to provide the upper bound for the latter (otherwise
* you could call `expect<String>()`).
* For syntactic reasons, Kotlin's compiler will refuse to call a 2-argument method with only one
* argument, even if it can infer the first type argument. The proxy methods (like the
* `inline fun <reified T : Event> expect(...)`) solve this by providing a single-argument version
* that calls the 2-type argument version instructing the compiler to infer the first argument
* (`expect<_, T>` infers `_`). This lets you write `expect<MySpecificEvent>()`.
*/
/**
* An interface for classes on which the expect* and assertNo* family of methods can be called.
*/
// TODO : why do we have to specify `: Any` here ? Is that not the default ? If you remove
// it then `KClass<T>` complains that T is not within its `Any` bounds,
interface Expectable<E : Any> {
val history: ArrayTrackRecord<E>.ReadHead
val defaultTimeoutMs: Long
val defaultNoEventTimeoutMs: Long
/*****
* Expect an event immediately
*
* This method fetches the next callback and assert it matches the conditions : type,
* passed predicate. If no callback is received within the timeout, this method fails.
*/
fun <T : E> expect(
type: KClass<T>,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
predicate: (T) -> Boolean = { true }
): T
/*****
* Expect that some event is received eventually.
*
* This method makes sure a callback that matches the type/predicate is received eventually.
* Any callback of the wrong type, or doesn't match the optional predicate, is ignored.
* It fails if no callback matching the predicate is received within the timeout.
*/
fun <T : E> eventuallyExpect(
type: KClass<T>,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
predicate: (T) -> Boolean = { true }
): T
/*****
* Assert no event matching a predicate was received.
*
* This method makes sure that no callback that matches the predicate was received.
* If no predicate is given, it makes sure that no callback at all was received.
*/
fun <T : E> assertNo(
type: KClass<T>,
timeoutMs: Long = defaultNoEventTimeoutMs,
errorMsg: String? = null,
predicate: (T) -> Boolean = { true }
)
fun assertNoCallback(
timeoutMs: Long = defaultNoEventTimeoutMs,
errorMsg: String? = null,
predicate: (E) -> Boolean = { true }
)
}
inline fun <E : Any, reified T : E> Expectable<E>.expect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = expect(T::class, timeoutMs, errorMsg, predicate)
inline fun <E : Any, reified T : E> Expectable<E>.eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = eventuallyExpect(T::class, timeoutMs, errorMsg, predicate)
inline fun <E : Any, reified T : E> Expectable<E>.assertNo(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = assertNo(T::class, timeoutMs, errorMsg, predicate)
/**
* A base class for connectivity testable callbacks.
*/
open class TestableCallback<E : Any>(
override val defaultTimeoutMs: Long = DEFAULT_TIMEOUT,
override val defaultNoEventTimeoutMs: Long = DEFAULT_NO_EVENT_TIMEOUT
) : Expectable<E> {
override val history = ArrayTrackRecord<E>().newReadHead()
val mark get() = history.mark
/**
* Implementation of the expect<> family of methods. All other
* methods eventually call this. They implement the Expectable<E>
* interface.
*/
override fun <T : E> expect(
type: KClass<T>,
timeoutMs: Long,
errorMsg: String?,
predicate: (T) -> Boolean
): T {
val event = history.poll(timeoutMs)
if (null == event) {
failWithErrorReason(
errorMsg,
"Did not receive ${type.simpleName} after ${timeoutMs}ms"
)
}
if (!type.isInstance(event)) {
failWithErrorReason(
errorMsg,
"Expected callback ${type.simpleName}, got $event"
)
}
event as T
if (!predicate(event)) {
failWithErrorReason(errorMsg, "Callback doesn't match predicate : $event")
}
return event
}
override fun <T : E> eventuallyExpect(
type: KClass<T>,
timeoutMs: Long,
errorMsg: String?,
predicate: (T) -> Boolean,
): T {
val event = history.poll(timeoutMs) { type.isInstance(it) && predicate(it as T) }
if (null == event) {
failWithErrorReason(
errorMsg,
"Callback ${type.simpleName} not received within ${timeoutMs}ms. " +
"Got \n${history.prettyPrintBacktrace(before = 4, after = 4)}"
)
}
return event as T
}
override fun assertNoCallback(
timeoutMs: Long,
errorMsg: String?,
predicate: (E) -> Boolean,
) {
val mark = history.mark
try {
val event = history.poll(timeoutMs) { predicate(it) }
if (null != event) {
failWithErrorReason(
errorMsg,
"Expected no such callback but found $event"
)
}
} finally {
// Rewind so callers can expect a failure and continue later. For example, a caller
// might want to assert no callback { interfaceName == "wlan1" } but then go on
// to test that some other event took place, which would not be possible without this
// rewind because it would have been skipped by the `poll` call above.
history.rewind(mark)
}
}
override fun <T : E> assertNo(
type: KClass<T>,
timeoutMs: Long,
errorMsg: String?,
predicate: (T) -> Boolean,
) = assertNoCallback(timeoutMs, errorMsg) { type.isInstance(it) && predicate(it as T) }
/**
* Proxy methods to avoid having to write expect<_, T>, see comment at the top of this file.
*/
inline fun <reified T : E> expect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = expect<_, T>(timeoutMs, errorMsg, predicate)
inline fun <reified T : E> eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = eventuallyExpect<_, T>(timeoutMs, errorMsg, predicate)
inline fun <reified T : E> assertNo(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
noinline predicate: (T) -> Boolean = { true }
) = assertNo<_, T>(timeoutMs, errorMsg, predicate)
}

View file

@ -345,6 +345,45 @@ class ArrayTrackRecord<E> : TrackRecord<E> {
}
}
/**
* Returns a pretty-printed list of events.
* @param before how many events before the last poll() operation to return
* @param after how many events after the mark to return
* @param width how many columns wide the string can be
* @return a string to embed in an error message.
*/
fun prettyPrintBacktrace(before: Int = 0, after: Int = 0, width: Int = 300): String {
val sb = StringBuilder()
val stamp = slock.tryReadLock()
if (0L == stamp) concurrentAccessDetected()
val from = pollMark
val to = mark
val start = (from - before).coerceAtLeast(0)
val end = (to + after).coerceAtMost(size - 1)
try {
lock.withLock {
for (i in start..end) {
when (i) {
from -> sb.append("< ")
to -> sb.append("> ")
else -> sb.append(" ")
}
sb.append(get(i).toString().safeSubstring(0..width))
sb.append("\n")
}
}
} finally {
slock.unlockRead(stamp)
}
if (from == size) {
sb.append("<>\n")
} else if (to == size) {
sb.append("> \n")
}
return sb.toString()
}
/**
* Returns the first element after the mark or null. This never blocks.
*
@ -356,5 +395,7 @@ class ArrayTrackRecord<E> : TrackRecord<E> {
}
}
// Private helper
// Private helpers
private fun Condition.await(timeoutMs: Long) = this.await(timeoutMs, TimeUnit.MILLISECONDS)
private fun String.safeSubstring(range: IntRange) =
substring(range.start..range.endInclusive.coerceAtMost(length - 1))

View file

@ -198,7 +198,7 @@ public class VpnTest {
private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname";
private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier";
private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
private static final long NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
private static final LinkAddress TEST_IP4_DST_ADDR = new LinkAddress("198.51.100.1/24");
private static final LinkAddress TEST_IP4_SRC_ADDR = new LinkAddress("198.51.100.2/24");
@ -1945,18 +1945,18 @@ public class VpnTest {
expectVpnNetwork(defaultNetworkCallback);
}, /* cleanup */ () -> {
stopVpn();
defaultNetworkCallback.eventuallyExpect(Event.LOST);
}, /* cleanup */ () -> {
runWithShellPermissionIdentity(() -> {
mCM.setVpnDefaultForUids(session, new ArraySet<>());
}, NETWORK_SETTINGS);
// The default network of the app will be changed back to wifi when the VPN network
// preference feature is disabled.
defaultNetworkCallback.eventuallyExpect(Event.AVAILABLE,
NETWORK_CALLBACK_TIMEOUT_MS,
entry -> defaultNetwork.equals(entry.getNetwork()));
});
stopVpn();
defaultNetworkCallback.eventuallyExpect(Event.LOST);
}, /* cleanup */ () -> {
runWithShellPermissionIdentity(() -> {
mCM.setVpnDefaultForUids(session, new ArraySet<>());
}, NETWORK_SETTINGS);
// The default network of the app will be changed back to wifi when the VPN network
// preference feature is disabled.
defaultNetworkCallback.eventuallyExpect(Event.AVAILABLE,
NETWORK_CALLBACK_TIMEOUT_MS,
entry -> defaultNetwork.equals(entry.getNetwork()));
});
}
/**

View file

@ -318,7 +318,7 @@ public class ConnectivityManagerTest {
private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
private static final int MIN_KEEPALIVE_INTERVAL = 10;
private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
private static final long NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
// Timeout for waiting network to be validated.
private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
private static final int NO_CALLBACK_TIMEOUT_MS = 100;
@ -481,11 +481,9 @@ public class ConnectivityManagerTest {
// networkCallbackRule is the outer rule and will be cleaned up after this method.
final TestableNetworkCallback callback =
networkCallbackRule.registerDefaultNetworkCallback();
assertNotNull("Couldn't restore Internet connectivity",
callback.eventuallyExpect(Event.NETWORK_CAPS_UPDATED,
NETWORK_CALLBACK_TIMEOUT_MS,
entry -> ((Event.CapabilitiesChanged) entry)
.getCaps().hasCapability(NET_CAPABILITY_VALIDATED)));
callback.eventuallyExpect(Event.NETWORK_CAPS_UPDATED,
NETWORK_CALLBACK_TIMEOUT_MS,
entry -> entry.getCaps().hasCapability(NET_CAPABILITY_VALIDATED));
});
}

View file

@ -49,7 +49,7 @@ import com.android.testutils.TestableNetworkCallback
import com.android.testutils.TestableNetworkCallback.Event.Lost
import com.android.testutils.TestableNetworkCallback.Event.Reserved
import com.android.testutils.TestableNetworkCallback.Event.Unavailable
import com.android.testutils.anyNetwork
import com.android.testutils.AnyNetwork
import com.android.testutils.waitForIdle
import java.io.IOException
import java.util.Optional
@ -306,7 +306,7 @@ class CSL2capProviderTest : CSTest() {
doReturn(true).`when`(btSocket).isConnected()
acceptQueue.put(Optional.of(btSocket))
cb.expectAvailableCallbacks(anyNetwork(), validated = false)
cb.expectAvailableCallbacks(AnyNetwork(), validated = false)
cb.assertNoCallback()
// Verify that packet forwarding was started.
// TODO: stop mocking L2capPacketForwarder.
@ -355,7 +355,7 @@ class CSL2capProviderTest : CSTest() {
.build()
val nr = REQUEST.copyWithSpecifier(specifier)
val cb = requestNetwork(nr)
cb.expectAvailableCallbacks(anyNetwork(), validated = false)
cb.expectAvailableCallbacks(AnyNetwork(), validated = false)
}
@Test
@ -368,7 +368,7 @@ class CSL2capProviderTest : CSTest() {
.build()
var nr = REQUEST.copyWithSpecifier(specifier)
val cb = requestNetwork(nr)
cb.expectAvailableCallbacks(anyNetwork(), validated = false)
cb.expectAvailableCallbacks(AnyNetwork(), validated = false)
specifier = L2capNetworkSpecifier.Builder()
.setRole(ROLE_CLIENT)
@ -391,10 +391,10 @@ class CSL2capProviderTest : CSTest() {
.build()
val nr = REQUEST.copyWithSpecifier(specifier)
val cb = requestNetwork(nr)
cb.expectAvailableCallbacks(anyNetwork(), validated = false)
cb.expectAvailableCallbacks(AnyNetwork(), validated = false)
val cb2 = requestNetwork(nr)
cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
cb2.expectAvailableCallbacks(AnyNetwork(), validated = false)
}
/** Test to ensure onLost() is sent before onUnavailable() when the network is torn down. */
@ -409,7 +409,7 @@ class CSL2capProviderTest : CSTest() {
val nr = REQUEST.copyWithSpecifier(specifier)
val cb = requestNetwork(nr)
cb.expectAvailableCallbacks(anyNetwork(), validated = false)
cb.expectAvailableCallbacks(AnyNetwork(), validated = false)
// Capture the L2capPacketForwarder callback object to tear down the network.
val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)