blob: 533ec224c8767e12f91c18ddf6672b2c78329001 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.testutils
import android.net.ConnectivityManager.NetworkCallback
import android.net.LinkProperties
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.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
import com.android.testutils.RecorderCallback.CallbackEntry.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
open class RecorderCallback private constructor(
private val backingRecord: ArrayTrackRecord<CallbackEntry>
) : NetworkCallback() {
public constructor() : this(ArrayTrackRecord())
protected constructor(src: RecorderCallback?) : this(src?.backingRecord ?: ArrayTrackRecord())
private val TAG = this::class.simpleName
sealed class CallbackEntry {
// To get equals(), hashcode(), componentN() etc for free, the child classes of
// this class are data classes. But while data classes can inherit from other classes,
// they may only have visible members in the constructors, so they couldn't declare
// a constructor with a non-val arg to pass to CallbackEntry. Instead, force all
// subclasses to implement a `network' property, which can be done in a data class
// constructor by specifying override.
abstract val network: Network
data class Available(override val network: Network) : CallbackEntry()
data class CapabilitiesChanged(
override val network: Network,
val caps: NetworkCapabilities
) : CallbackEntry()
data class LinkPropertiesChanged(
override val network: Network,
val lp: LinkProperties
) : CallbackEntry()
data class Suspended(override val network: Network) : CallbackEntry()
data class Resumed(override val network: Network) : CallbackEntry()
data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
data class Lost(override val network: Network) : CallbackEntry()
data class Unavailable private constructor(
override val network: Network
) : CallbackEntry() {
constructor() : this(NULL_NETWORK)
}
data class BlockedStatus(
override val network: Network,
val blocked: Boolean
) : CallbackEntry()
data class BlockedStatusInt(
override val network: Network,
val blocked: Int
) : CallbackEntry()
// Convenience constants for expecting a type
companion object {
@JvmField
val AVAILABLE = Available::class
@JvmField
val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
@JvmField
val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
@JvmField
val SUSPENDED = Suspended::class
@JvmField
val RESUMED = Resumed::class
@JvmField
val LOSING = Losing::class
@JvmField
val LOST = Lost::class
@JvmField
val UNAVAILABLE = Unavailable::class
@JvmField
val BLOCKED_STATUS = BlockedStatus::class
@JvmField
val BLOCKED_STATUS_INT = BlockedStatusInt::class
}
}
val history = backingRecord.newReadHead()
val mark get() = history.mark
override fun onAvailable(network: Network) {
Log.d(TAG, "onAvailable $network")
history.add(Available(network))
}
// PreCheck is not used in the tests today. For backward compatibility with existing tests that
// expect the callbacks not to record this, do not listen to PreCheck here.
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
Log.d(TAG, "onCapabilitiesChanged $network $caps")
history.add(CapabilitiesChanged(network, caps))
}
override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
Log.d(TAG, "onLinkPropertiesChanged $network $lp")
history.add(LinkPropertiesChanged(network, lp))
}
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
Log.d(TAG, "onBlockedStatusChanged $network $blocked")
history.add(BlockedStatus(network, blocked))
}
// Cannot do:
// fun onBlockedStatusChanged(network: Network, blocked: Int) {
// because on S, that needs to be "override fun", and on R, that cannot be "override fun".
override fun onNetworkSuspended(network: Network) {
Log.d(TAG, "onNetworkSuspended $network $network")
history.add(Suspended(network))
}
override fun onNetworkResumed(network: Network) {
Log.d(TAG, "$network onNetworkResumed $network")
history.add(Resumed(network))
}
override fun onLosing(network: Network, maxMsToLive: Int) {
Log.d(TAG, "onLosing $network $maxMsToLive")
history.add(Losing(network, maxMsToLive))
}
override fun onLost(network: Network) {
Log.d(TAG, "onLost $network")
history.add(Lost(network))
}
override fun onUnavailable() {
Log.d(TAG, "onUnavailable")
history.add(Unavailable())
}
}
private const val DEFAULT_TIMEOUT = 30_000L // ms
private const val DEFAULT_NO_CALLBACK_TIMEOUT = 200L // ms
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 = DEFAULT_TIMEOUT,
val defaultNoCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
val waiterFunc: Runnable = NOOP // "() -> Unit" would forbid calling with a void func from Java
) : RecorderCallback(src) {
/**
* Construct a testable network callback.
* @param timeoutMs the default timeout for expecting a callback. Default 30 seconds. This
* should be long in most cases, because the success case doesn't incur
* the wait.
* @param noCallbackTimeoutMs the timeout for expecting that no callback is received. Default
* 200ms. Because the success case does incur the timeout, this
* should be short in most cases, but not so short as to frequently
* time out before an incorrect callback is received.
* @param waiterFunc a function to use before asserting no callback. For some specific tests,
* it is useful to run test-specific code before asserting no callback to
* increase the likelihood that a spurious callback is correctly detected.
* As an example, a unit test using mock loopers may want to use this to
* make sure the loopers are drained before asserting no callback, since
* one of them may cause a callback to be called. @see ConnectivityServiceTest
* for such an example.
*/
@JvmOverloads
constructor(
timeoutMs: Long = DEFAULT_TIMEOUT,
noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
waiterFunc: Runnable = NOOP
) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc)
fun createLinkedCopy() = TestableNetworkCallback(
this, defaultTimeoutMs, defaultNoCallbackTimeoutMs, waiterFunc)
// 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
val lastAvailableNetwork: Network?
get() = when (val it = history.lastOrNull { it is Available || it is Lost }) {
is Available -> it.network
else -> null
}
/**
* Get the next callback or null if timeout.
*
* With no argument, this method waits out the default timeout. To wait forever, pass
* Long.MAX_VALUE.
*/
@JvmOverloads
fun poll(timeoutMs: Long = defaultTimeoutMs): CallbackEntry? = history.poll(timeoutMs)
/**
* Get the next callback or throw if timeout.
*
* With no argument, this method waits out the default timeout. To wait forever, pass
* Long.MAX_VALUE.
*/
@JvmOverloads
fun pollOrThrow(
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String = "Did not receive callback after $timeoutMs"
): CallbackEntry = poll(timeoutMs) ?: fail(errorMsg)
/*****
* 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.
*/
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: Network = ANY_NETWORK,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = expect<CallbackEntry>(network, timeoutMs, errorMsg) {
test(it as? T ?: fail("Expected callback ${type.simpleName}, got $it"))
} as T
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: HasNetwork,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = expect(type, network.network, timeoutMs, errorMsg, test)
// Java needs an explicit overload to let it omit arguments in the middle, so define these
// 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 : CallbackEntry> expect(
type: KClass<T>,
timeoutMs: Long,
errorMsg: String?,
test: (T) -> Boolean = { true }
) = expect(type, ANY_NETWORK, timeoutMs, errorMsg, test)
// Without |timeout|, in Network and HasNetwork versions
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: Network,
errorMsg: String?,
test: (T) -> Boolean = { true }
) = expect(type, network, defaultTimeoutMs, errorMsg, test)
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: HasNetwork,
errorMsg: String?,
test: (T) -> Boolean = { true }
) = expect(type, network.network, defaultTimeoutMs, errorMsg, test)
// Without |errorMsg|, in Network and HasNetwork versions
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: Network,
timeoutMs: Long,
test: (T) -> Boolean
) = expect(type, network, timeoutMs, null, test)
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: HasNetwork,
timeoutMs: Long,
test: (T) -> Boolean
) = expect(type, network.network, timeoutMs, null, test)
// Without |network| or |timeout|
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
errorMsg: String?,
test: (T) -> Boolean = { true }
) = expect(type, ANY_NETWORK, defaultTimeoutMs, errorMsg, test)
// Without |network| or |errorMsg|
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
timeoutMs: Long,
test: (T) -> Boolean = { true }
) = expect(type, ANY_NETWORK, timeoutMs, null, test)
// Without |timeout| or |errorMsg|, in Network and HasNetwork versions
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: Network,
test: (T) -> Boolean
) = expect(type, network, defaultTimeoutMs, null, test)
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
network: HasNetwork,
test: (T) -> Boolean
) = expect(type, network.network, defaultTimeoutMs, null, test)
// Without |network| or |timeout| or |errorMsg|
@JvmOverloads
fun <T : CallbackEntry> expect(
type: KClass<T>,
test: (T) -> Boolean
) = expect(type, ANY_NETWORK, defaultTimeoutMs, null, test)
// Kotlin reified versions. Don't call methods above, or the predicate would need to be noinline
inline fun <reified T : CallbackEntry> expect(
network: Network = ANY_NETWORK,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = pollOrThrow(timeoutMs, "Did not receive ${T::class.simpleName} after ${timeoutMs}ms").also {
if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
if (ANY_NETWORK !== network && it.network != network) {
fail("Expected network $network for callback : $it")
}
if (!test(it)) {
fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
}
} as T
inline fun <reified T : CallbackEntry> expect(
network: HasNetwork,
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
) = expect(network.network, timeoutMs, errorMsg, test)
@JvmOverloads
fun assertNoCallback(
timeoutMs: Long = defaultNoCallbackTimeoutMs,
valid: (CallbackEntry) -> Boolean = { true }
) {
waiterFunc.run()
history.poll(timeoutMs) { valid(it) }?.let { fail("Expected no callback but got $it") }
}
fun assertNoCallback(valid: (CallbackEntry) -> Boolean) =
assertNoCallback(defaultNoCallbackTimeoutMs, valid)
// Expects a callback of the specified type matching the predicate within the timeout.
// Any callback that doesn't match the predicate will be skipped. Fails only if
// no matching callback is received within the timeout.
inline fun <reified T : CallbackEntry> eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
from: Int = mark,
crossinline predicate: (T) -> Boolean = { true }
): T = eventuallyExpectOrNull(timeoutMs, from, predicate).also {
assertNotNull(it, "Callback ${T::class} not received within ${timeoutMs}ms")
} as T
@JvmOverloads
fun <T : CallbackEntry> 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")
} as T
fun <T : CallbackEntry> 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")
} as T
// TODO (b/157405399) straighten and unify the method names
inline fun <reified T : CallbackEntry> eventuallyExpectOrNull(
timeoutMs: Long = defaultTimeoutMs,
from: Int = mark,
crossinline predicate: (T) -> Boolean = { true }
) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T?
inline fun expectLinkPropertiesThat(
net: Network,
tmt: Long = defaultTimeoutMs,
valid: (LinkProperties) -> Boolean
): LinkPropertiesChanged =
expect(net, tmt, "LinkProperties don't match expectations") { valid(it.lp) }
// Expects onAvailable and the callbacks that follow it. These are:
// - onSuspended, iff the network was suspended when the callbacks fire.
// - onCapabilitiesChanged.
// - onLinkPropertiesChanged.
// - onBlockedStatusChanged.
//
// @param network the network to expect the callbacks on.
// @param suspended whether to expect a SUSPENDED callback.
// @param validated the expected value of the VALIDATED capability in the
// onCapabilitiesChanged callback.
// @param tmt how long to wait for the callbacks.
@JvmOverloads
fun expectAvailableCallbacks(
net: Network,
suspended: Boolean = false,
validated: Boolean? = true,
blocked: Boolean = false,
tmt: Long = defaultTimeoutMs
) {
expectAvailableCallbacksCommon(net, suspended, validated, tmt)
expectBlockedStatusCallback(blocked, net, tmt)
}
fun expectAvailableCallbacks(
net: Network,
suspended: Boolean,
validated: Boolean,
blockedStatus: Int,
tmt: Long
) {
expectAvailableCallbacksCommon(net, suspended, validated, tmt)
expectBlockedStatusCallback(blockedStatus, net)
}
private fun expectAvailableCallbacksCommon(
net: Network,
suspended: Boolean,
validated: Boolean?,
tmt: Long
) {
expect<Available>(net, tmt)
if (suspended) {
expect<Suspended>(net, tmt)
}
expect<CapabilitiesChanged>(net, tmt) {
validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
}
expect<LinkPropertiesChanged>(net, tmt)
}
// Backward compatibility for existing Java code. Use named arguments instead and remove all
// these when there is no user left.
fun expectAvailableAndSuspendedCallbacks(
net: Network,
validated: Boolean,
tmt: Long = defaultTimeoutMs
) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) =
expect<BlockedStatus>(net, tmt, "Unexpected blocked status") {
it.blocked == blocked
}
fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) =
expect<BlockedStatusInt>(net, tmt, "Unexpected blocked status") {
it.blocked == blocked
}
// Expects the available callbacks (where the onCapabilitiesChanged must contain the
// VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
// one we just sent.
// TODO: this is likely a bug. Fix it and remove this method.
fun expectAvailableDoubleValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
val mark = history.mark
expectAvailableCallbacks(net, tmt = tmt)
val firstCaps = history.poll(tmt, mark) { it is CapabilitiesChanged }
assertEquals(firstCaps, expect<CapabilitiesChanged>(net, tmt))
}
// Expects the available callbacks where the onCapabilitiesChanged must not have validated,
// then expects another onCapabilitiesChanged that has the validated bit set. This is used
// when a network connects and satisfies a callback, and then immediately validates.
fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
expectAvailableCallbacks(net, validated = false, tmt = tmt)
expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
}
fun expectAvailableThenValidatedCallbacks(
net: Network,
blockedStatus: Int,
tmt: Long = defaultTimeoutMs
) {
expectAvailableCallbacks(net, validated = false, suspended = false,
blockedStatus = blockedStatus, tmt = tmt)
expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
}
// Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
// calls with networkAgent can be routed through here without moving MockNetworkAgent.
// TODO: clean this up, remove this method.
interface HasNetwork {
val network: Network
}
fun expectAvailableCallbacks(
n: HasNetwork,
suspended: Boolean,
validated: Boolean,
blocked: Boolean,
timeoutMs: Long
) = expectAvailableCallbacks(n.network, suspended, validated, blocked, timeoutMs)
fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
}
fun expectAvailableCallbacksValidated(n: HasNetwork) {
expectAvailableCallbacks(n.network)
}
fun expectAvailableCallbacksValidatedAndBlocked(n: HasNetwork) {
expectAvailableCallbacks(n.network, blocked = true)
}
fun expectAvailableCallbacksUnvalidated(n: HasNetwork) {
expectAvailableCallbacks(n.network, validated = false)
}
fun expectAvailableCallbacksUnvalidatedAndBlocked(n: HasNetwork) {
expectAvailableCallbacks(n.network, validated = false, blocked = true)
}
fun expectAvailableDoubleValidatedCallbacks(n: HasNetwork) {
expectAvailableDoubleValidatedCallbacks(n.network, defaultTimeoutMs)
}
fun expectAvailableThenValidatedCallbacks(n: HasNetwork) {
expectAvailableThenValidatedCallbacks(n.network, defaultTimeoutMs)
}
@JvmOverloads
fun expectLinkPropertiesThat(
n: HasNetwork,
tmt: Long = defaultTimeoutMs,
valid: (LinkProperties) -> Boolean
) = expectLinkPropertiesThat(n.network, tmt, valid)
@JvmOverloads
fun expectCaps(
n: HasNetwork,
tmt: Long = defaultTimeoutMs,
valid: (NetworkCapabilities) -> Boolean = { true }
) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps
@JvmOverloads
fun expectCaps(
n: Network,
tmt: Long = defaultTimeoutMs,
valid: (NetworkCapabilities) -> Boolean
) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps
fun expectCaps(
n: HasNetwork,
valid: (NetworkCapabilities) -> Boolean
) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps
fun expectCaps(
tmt: Long,
valid: (NetworkCapabilities) -> Boolean
) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) {
expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs)
}
}