diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt index c35013da86..c7661d49bc 100644 --- a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt +++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt @@ -122,6 +122,7 @@ class TestableNetworkCallbackTest { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) mCallback.onAvailable(Network(100)) assertFails { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) } + mCallback.expect() 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(ANY_NETWORK, 10, "error") { true } + mCallback.expect(AnyNetwork, 10, "error") { true } mCallback.expect(timeoutMs = 10, errorMsg = "error") { true } - mCallback.expect(network = ANY_NETWORK, errorMsg = "error") { true } - mCallback.expect(network = ANY_NETWORK, timeoutMs = 10) { true } - mCallback.expect(network = ANY_NETWORK, timeoutMs = 10, errorMsg = "error") + mCallback.expect(network = AnyNetwork, errorMsg = "error") { true } + mCallback.expect(network = AnyNetwork, timeoutMs = 10) { true } + mCallback.expect(network = AnyNetwork, timeoutMs = 10, errorMsg = "error") mCallback.expect(errorMsg = "error") { true } mCallback.expect(timeoutMs = 10) { true } mCallback.expect(timeoutMs = 10, errorMsg = "error") - mCallback.expect(network = ANY_NETWORK) { true } - mCallback.expect(network = ANY_NETWORK, errorMsg = "error") - mCallback.expect(network = ANY_NETWORK, timeoutMs = 10) + mCallback.expect(network = AnyNetwork) { true } + mCallback.expect(network = AnyNetwork, errorMsg = "error") + mCallback.expect(network = AnyNetwork, timeoutMs = 10) mCallback.expect { true } mCallback.expect(errorMsg = "error") mCallback.expect(timeoutMs = 10) - mCallback.expect(network = ANY_NETWORK) + mCallback.expect(network = AnyNetwork) mCallback.expect() mCallback.expect(hn, 10, "error") { true } diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java index da1cbe4dc0..8f29628068 100644 --- a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java +++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTestJava.java @@ -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); diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt index de175ce15d..a784b8a0d8 100644 --- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt +++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt @@ -81,7 +81,7 @@ class ConnectivityCheckTest { .addCapability(NET_CAPABILITY_INTERNET) .build() ) - val capChanged = cb.eventuallyExpect(from = 0) + val capChanged = cb.eventuallyExpect() val network = capChanged.network val ssid = capChanged.caps.ssid assertFalse(ssid.isNullOrEmpty(), "No SSID for wifi network $network") diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt index ccf72c8a7d..7c872f4916 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt @@ -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 = - src?.backingRecord ?: ArrayTrackRecord() - val history: ArrayTrackRecord.ReadHead = backingRecord.newReadHead() + private val logTag: String, + val callbackTracker: TestableCallback = TestableCallback( + defaultCallbackTimeoutMs, + defaultCallbackNoEventTimeoutMs + ) +) : NetworkCallback(), Expectable 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 expect( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } + ) = expect<_, T>(timeoutMs, errorMsg, predicate) + + inline fun eventuallyExpect( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } + ) = eventuallyExpect<_, T>(timeoutMs, errorMsg, predicate) + + inline fun 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 expect( type: KClass, - network: Network = ANY_NETWORK, + network: Network = AnyNetwork, timeoutMs: Long = defaultTimeoutMs, errorMsg: String? = null, test: (T) -> Boolean = { true } - ) = expect(network, timeoutMs, errorMsg) { - if (type.isInstance(it)) { - test(it as T) // Cast can't fail since type.isInstance(it) and type: KClass - } 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 expect( + network: Network = AnyNetwork, + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline test: (T) -> Boolean = { true } + ) = expect(T::class, network, timeoutMs, errorMsg, test) + + inline fun expect( + network: HasNetwork, + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline test: (T) -> Boolean = { true } + ) = expect(T::class, network.network, timeoutMs, errorMsg, test) @JvmOverloads fun 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 expect( type: KClass, 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, 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, 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 expect( @@ -360,37 +400,54 @@ open class TestableNetworkCallback private constructor( fun expect( type: KClass, 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 expect( - network: Network = ANY_NETWORK, + /***** + * eventuallyExpect family of methods, to support Java code + * + * See Expectable#eventuallyExpect + */ + @JvmOverloads + fun eventuallyExpect( + type: KClass, 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 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 eventuallyExpect( + type: KClass, + 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 eventuallyExpect( + type: KClass, + from: Int, + predicate: (T) -> Boolean + ): T = eventuallyExpect(type, timeoutMs = defaultTimeoutMs, from, predicate) + + inline fun 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 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 eventuallyExpect( - type: KClass, - 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 eventuallyExpect( - type: KClass, - 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(ANY_NETWORK, tmt) { valid(it.caps) }.caps + ) = expect(AnyNetwork, tmt) { valid(it.caps) }.caps } diff --git a/staticlibs/testutils/hostdevice/com/android/net/module/util/TestableCallback.kt b/staticlibs/testutils/hostdevice/com/android/net/module/util/TestableCallback.kt new file mode 100644 index 0000000000..5e281d0efc --- /dev/null +++ b/staticlibs/testutils/hostdevice/com/android/net/module/util/TestableCallback.kt @@ -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(), 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() { it.data.startsWith("foo") }` + * // Wait for any `SecondAction` event for up to 500 milliseconds. + * `myTestServiceCallback.expect(timeoutMs = 500)` + * // Make sure no `SecondAction` event has fired so far. + * `myTestServiceCallback.assertNo() + * 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) + * : TestedServiceCallback, Expectable by internalEventTracker { ... + * ``` + * 3. **Proxy** the `expect`, `eventuallyExpect`, and `assertNo` methods. This makes your test code + * simpler by allowing you to write `expect()` 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 = TestableCallback() + * ) : TestedServiceCallback(), Expectable 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 expect( + * timeoutMs: Long = defaultTimeoutMs, + * errorMsg: String? = null, + * noinline predicate: (T) -> Boolean = { true } + * ) = expect<_, T>(timeoutMs, errorMsg, predicate) + * + * inline fun eventuallyExpect( + * timeoutMs: Long = defaultTimeoutMs, + * errorMsg: String? = null, + * noinline predicate: (T) -> Boolean = { true } + * ) = eventuallyExpect<_, T>(timeoutMs, errorMsg, predicate) + * + * inline fun 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`. When you inherit from `TestableCallback`, the type `Event` is + * directly known, making calls like `expect()` 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()`). + * 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 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()`. + */ + +/** + * 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` complains that T is not within its `Any` bounds, +interface Expectable { + val history: ArrayTrackRecord.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 expect( + type: KClass, + 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 eventuallyExpect( + type: KClass, + 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 assertNo( + type: KClass, + timeoutMs: Long = defaultNoEventTimeoutMs, + errorMsg: String? = null, + predicate: (T) -> Boolean = { true } + ) + + fun assertNoCallback( + timeoutMs: Long = defaultNoEventTimeoutMs, + errorMsg: String? = null, + predicate: (E) -> Boolean = { true } + ) +} + +inline fun Expectable.expect( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } +) = expect(T::class, timeoutMs, errorMsg, predicate) + +inline fun Expectable.eventuallyExpect( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } +) = eventuallyExpect(T::class, timeoutMs, errorMsg, predicate) + +inline fun Expectable.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( + override val defaultTimeoutMs: Long = DEFAULT_TIMEOUT, + override val defaultNoEventTimeoutMs: Long = DEFAULT_NO_EVENT_TIMEOUT +) : Expectable { + override val history = ArrayTrackRecord().newReadHead() + val mark get() = history.mark + + /** + * Implementation of the expect<> family of methods. All other + * methods eventually call this. They implement the Expectable + * interface. + */ + override fun expect( + type: KClass, + 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 eventuallyExpect( + type: KClass, + 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 assertNo( + type: KClass, + 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 expect( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } + ) = expect<_, T>(timeoutMs, errorMsg, predicate) + + inline fun eventuallyExpect( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } + ) = eventuallyExpect<_, T>(timeoutMs, errorMsg, predicate) + + inline fun assertNo( + timeoutMs: Long = defaultTimeoutMs, + errorMsg: String? = null, + noinline predicate: (T) -> Boolean = { true } + ) = assertNo<_, T>(timeoutMs, errorMsg, predicate) +} diff --git a/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt index 123efd7283..82b8f43e6f 100644 --- a/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt +++ b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt @@ -345,6 +345,45 @@ class ArrayTrackRecord : TrackRecord { } } + /** + * 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 : TrackRecord { } } -// 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)) diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java index 19f0a0c2b4..01932a4b81 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java @@ -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())); + }); } /** diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java index a758b57b6c..e25eddb63d 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java @@ -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)); }); } diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt index 98e7ff44ae..c2bacbac21 100644 --- a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt @@ -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)