Merge changes Iabc2fad0,I2078330b into main

* changes:
  Improve TestableNetworkOfferCallback
  Add a way to poll with a position for ReadHead
This commit is contained in:
Jean Chalard 2025-04-27 23:25:04 -07:00 • committed by Android (Google) Code Review
commit 74dcb4637c
5 changed files with 136 additions and 52 deletions

View file

@ -23,14 +23,20 @@ import android.util.Log
import com.android.net.module.util.ArrayTrackRecord
import kotlin.test.fail
class TestableNetworkOfferCallback(val timeoutMs: Long, private val noCallbackTimeoutMs: Long)
: NetworkProvider.NetworkOfferCallback {
private const val DEFAULT_TIMEOUT = 30_000L // ms
private const val DEFAULT_NO_CALLBACK_TIMEOUT = 200L // ms
class TestableNetworkOfferCallback(
val timeoutMs: Long = DEFAULT_TIMEOUT,
private val noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
) : NetworkProvider.NetworkOfferCallback {
private val TAG = this::class.simpleName
val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
val mark get() = history.mark
sealed class CallbackEntry {
data class OnNetworkNeeded(val request: NetworkRequest) : CallbackEntry()
data class OnNetworkUnneeded(val request: NetworkRequest) : CallbackEntry()
data class Needed(val request: NetworkRequest) : CallbackEntry()
data class Unneeded(val request: NetworkRequest) : CallbackEntry()
}
/**
@ -39,7 +45,7 @@ class TestableNetworkOfferCallback(val timeoutMs: Long, private val noCallbackTi
*/
override fun onNetworkNeeded(request: NetworkRequest) {
Log.d(TAG, "onNetworkNeeded $request")
history.add(CallbackEntry.OnNetworkNeeded(request))
history.add(CallbackEntry.Needed(request))
}
/**
@ -47,25 +53,59 @@ class TestableNetworkOfferCallback(val timeoutMs: Long, private val noCallbackTi
*/
override fun onNetworkUnneeded(request: NetworkRequest) {
Log.d(TAG, "onNetworkUnneeded $request")
history.add(CallbackEntry.OnNetworkUnneeded(request))
history.add(CallbackEntry.Unneeded(request))
}
// TODO : move to some utility, this is copied from TestableNetworkCallback
fun failWithErrorReason(errorMsg: String?, errorReason: String): Nothing {
val message = if (errorMsg != null) "$errorMsg : $errorReason" else errorReason
fail(message)
}
inline fun <reified T : CallbackEntry> expect(
errorMsg: String? = null,
predicate: (T) -> Boolean = { true }
): T {
val nextEntry = history.poll(timeoutMs) ?: failWithErrorReason(errorMsg,
"Did not receive ${T::class.simpleName} after ${timeoutMs}ms")
if (nextEntry !is T) {
failWithErrorReason(
errorMsg,
"Expected callback ${T::class.simpleName}, got $nextEntry"
)
}
if (!predicate(nextEntry)) {
failWithErrorReason(errorMsg, "Callback doesn't match predicate: $nextEntry")
}
return nextEntry
}
// TODO : remove when there are no callers
@Deprecated("Use expect instead of expectCallbackThat")
inline fun <reified T : CallbackEntry> expectCallbackThat(
crossinline predicate: (T) -> Boolean
): T {
val event = history.poll(timeoutMs)
?: fail("Did not receive callback after ${timeoutMs}ms")
if (event !is T || !predicate(event)) fail("Received unexpected callback $event")
return event
}
): T = expect<T>(null, predicate)
inline fun <reified T : CallbackEntry> eventuallyExpect(
errorMsg: String? = null,
from: Int = mark,
crossinline predicate: (T) -> Boolean = { true }
) = history.poll(timeoutMs, from) { it is T && predicate(it) }.also {
if (null == it) {
failWithErrorReason(
errorMsg,
"Callback ${T::class} not received within ${timeoutMs}ms."
)
}
} as T
fun expectOnNetworkNeeded(capabilities: NetworkCapabilities) =
expectCallbackThat<CallbackEntry.OnNetworkNeeded> {
expectCallbackThat<CallbackEntry.Needed> {
it.request.canBeSatisfiedBy(capabilities)
}
fun expectOnNetworkUnneeded(capabilities: NetworkCapabilities) =
expectCallbackThat<CallbackEntry.OnNetworkUnneeded> {
expectCallbackThat<CallbackEntry.Unneeded> {
it.request.canBeSatisfiedBy(capabilities)
}
@ -73,4 +113,4 @@ class TestableNetworkOfferCallback(val timeoutMs: Long, private val noCallbackTi
val cb = history.poll(noCallbackTimeoutMs)
if (null != cb) fail("Expected no callback but got $cb")
}
}
}

View file

@ -255,7 +255,7 @@ class ArrayTrackRecord<E> : TrackRecord<E> {
* the last element in the queue if null is returned. This means this method will always
* skip elements that do not match the predicate, even if it returns null.
*
* This method can only be used by the thread that created this ManagedRecordingQueue.
* This method can only be used by the thread that created this ReadHead.
* If used on another thread, this throws IllegalStateException.
*
* @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
@ -277,6 +277,55 @@ class ArrayTrackRecord<E> : TrackRecord<E> {
}
}
/**
* Returns the first element after the mark, optionally blocking until one is available,
* or null if no such element can be found within the timeout.
* If a predicate is given, only elements matching the predicate are returned.
*
* Upon return, the mark will be advanced up to the point of the returned element (or
* after the last element in the queue if not found) if and only if that element was
* found after the current mark. This means this method will always skip elements that
* do not match the predicate even if it returns null, and repeated calls with the
* same position will advance the mark up to the last of the found elements.
*
* This method can only be used by the thread that created this ReadHead.
* If used on another thread, this throws IllegalStateException.
*
* Typical usage would be to check for multiple elements in any order. For example,
* val mark = readHead.mark
* readHead.poll(timeout, mark) { it matches condition 1 }
* readHead.poll(timeout, mark) { it matches condition 2 }
* readHead.poll(timeout, mark) { it matches condition 3 }
* The above would check that starting at the mark, there are three events, one matching
* each condition, in any order. A caller could further check that there were no other
* event beside the 3 checked with something like
* assertEquals(readHead.mark - mark, 3)
*
* @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
* @param pos the position in the underlying track record where to start the search.
* @param predicate a predicate to filter elements to be returned.
* @return an element matching the predicate, or null if timeout.
*/
override fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean): E? {
val stamp = slock.tryWriteLock()
if (0L == stamp) concurrentAccessDetected()
pollMark = readHead
try {
lock.withLock {
val index = pollForIndexReadLocked(timeoutMs, pos, predicate)
when {
index < 0 -> readHead = size
index > readHead -> readHead = index + 1
// else do nothing, the element was found prior to the read head so
// don't move the read head
}
return getOrNull(index)
}
} finally {
slock.unlockWrite(stamp)
}
}
/**
* Returns a list of events that were observed since before the last poll() operation on
* this ReadHead.

View file

@ -177,10 +177,11 @@ class NetworkValidationTest {
cm.registerNetworkCallback(ethRequest, testCb)
try {
val ncCb = testCb.eventuallyExpect<CallbackEntry.CapabilitiesChanged> {
val mark = testCb.mark
val ncCb = testCb.eventuallyExpect<CallbackEntry.CapabilitiesChanged>(from = mark) {
it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
}
testCb.eventuallyExpect<CallbackEntry.LinkPropertiesChanged> {
testCb.eventuallyExpect<CallbackEntry.LinkPropertiesChanged>(from = mark) {
it.network == ncCb.network && it.lp.captivePortalData != null
}.lp
} finally {

View file

@ -36,7 +36,7 @@ import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.TestableNetworkOfferCallback
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.Needed
import kotlin.test.assertEquals
import kotlin.test.assertNull
import org.junit.Before
@ -134,7 +134,7 @@ class CSNetworkReservationTest : CSTest() {
// validate the offer does not receive onNetworkNeeded for reservation request
offerCb.expectNoCallbackWhere {
it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
it is Needed && it.request.type == NetworkRequest.Type.RESERVATION
}
}

View file

@ -44,8 +44,8 @@ import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.TestableNetworkOfferCallback
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkUnneeded
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.Needed
import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.Unneeded
import com.android.testutils.postAndWait
import com.android.testutils.runAsShell
import org.junit.Rule
@ -276,9 +276,9 @@ class CSPreferenceTest : CSTest() {
fun testOptInSatellitePreference_NetworkConnectedFirst_RestrictedSatellite() =
doTestSatellitePreference_NetworkConnectedFirst(satelliteRestricted = true)
private val OnNetworkNeeded.isRestricted
private val Needed.isRestricted
get() = !request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
private val OnNetworkUnneeded.isRestricted
private val Unneeded.isRestricted
get() = !request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
/**
@ -297,20 +297,14 @@ class CSPreferenceTest : CSTest() {
val provider = NetworkProvider(context, csHandlerThread.looper, "Test provider")
cm.registerNetworkProvider(provider)
val satelliteCallback = TestableNetworkOfferCallback(
DEFAULT_TIMEOUT_MS,
DEFAULT_NO_CALLBACK_TIMEOUT_MS
)
val satelliteCallback = TestableNetworkOfferCallback()
provider.registerNetworkOffer(
NetworkScore.Builder().build(),
satelliteNc(restricted = false),
Runnable::run,
satelliteCallback
)
val restrictedSatelliteCallback = TestableNetworkOfferCallback(
DEFAULT_TIMEOUT_MS,
DEFAULT_NO_CALLBACK_TIMEOUT_MS
)
val restrictedSatelliteCallback = TestableNetworkOfferCallback()
provider.registerNetworkOffer(
NetworkScore.Builder().build(),
satelliteNc(restricted = true),
@ -320,18 +314,18 @@ class CSPreferenceTest : CSTest() {
satelliteCallback.assertNoCallback()
updateSatellitePreference(emptySet(), setOf(UID1))
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { true }
satelliteCallback.expect<Needed>()
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
updateSatellitePreference(emptySet(), emptySet())
satelliteCallback.expectCallbackThat<OnNetworkUnneeded> { true }
satelliteCallback.expect<Unneeded>()
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
updateSatellitePreference(setOf(SMSUID), emptySet())
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
restrictedSatelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
satelliteCallback.expect<Needed> { it.isRestricted }
restrictedSatelliteCallback.expect<Needed> { it.isRestricted }
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
@ -346,39 +340,39 @@ class CSPreferenceTest : CSTest() {
// happening because updating the preferences removes the requests, which causes a
// rematch (where the requests are not registered), then adds the requests again, which
// causes another rematch.
satelliteCallback.expectCallbackThat<OnNetworkUnneeded> { it.isRestricted }
restrictedSatelliteCallback.expectCallbackThat<OnNetworkUnneeded> { it.isRestricted }
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { !it.isRestricted }
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
restrictedSatelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
satelliteCallback.expect<Unneeded> { it.isRestricted }
restrictedSatelliteCallback.expect<Unneeded> { it.isRestricted }
satelliteCallback.expect<Needed> { !it.isRestricted }
satelliteCallback.expect<Needed> { it.isRestricted }
restrictedSatelliteCallback.expect<Needed> { it.isRestricted }
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
updateSatellitePreference(emptySet(), setOf(UID1))
satelliteCallback.expectCallbackThat<OnNetworkUnneeded> { it.isRestricted }
satelliteCallback.expectCallbackThat<OnNetworkUnneeded> { !it.isRestricted }
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { !it.isRestricted }
restrictedSatelliteCallback.expectCallbackThat<OnNetworkUnneeded> { it.isRestricted }
satelliteCallback.expect<Unneeded> { it.isRestricted }
satelliteCallback.expect<Unneeded> { !it.isRestricted }
satelliteCallback.expect<Needed> { !it.isRestricted }
restrictedSatelliteCallback.expect<Unneeded> { it.isRestricted }
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
updateSatellitePreference(setOf(SMSUID), setOf(UID1))
restrictedSatelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
restrictedSatelliteCallback.expect<Needed> { it.isRestricted }
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
val wifiAgent = Agent(TRANSPORT_WIFI)
wifiAgent.connect()
satelliteCallback.expectCallbackThat<OnNetworkUnneeded> { it.isRestricted }
satelliteCallback.expectCallbackThat<OnNetworkUnneeded> { !it.isRestricted }
restrictedSatelliteCallback.expectCallbackThat<OnNetworkUnneeded> { it.isRestricted }
satelliteCallback.expect<Unneeded> { it.isRestricted }
satelliteCallback.expect<Unneeded> { !it.isRestricted }
restrictedSatelliteCallback.expect<Unneeded> { it.isRestricted }
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
wifiAgent.disconnect()
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
satelliteCallback.expectCallbackThat<OnNetworkNeeded> { !it.isRestricted }
restrictedSatelliteCallback.expectCallbackThat<OnNetworkNeeded> { it.isRestricted }
satelliteCallback.expect<Needed> { it.isRestricted }
satelliteCallback.expect<Needed> { !it.isRestricted }
restrictedSatelliteCallback.expect<Needed> { it.isRestricted }
satelliteCallback.assertNoCallback()
restrictedSatelliteCallback.assertNoCallback()
}