diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt new file mode 100644 index 0000000000..b532ee8b38 --- /dev/null +++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2022 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.annotation.SuppressLint +import android.net.LinkAddress +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import com.android.testutils.RecorderCallback.CallbackEntry +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.AVAILABLE +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.BLOCKED_STATUS +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LINK_PROPERTIES_CHANGED +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOSING +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CAPS_UPDATED +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOST +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED +import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE +import com.android.testutils.RecorderCallback.CallbackEntry.Available +import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus +import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail + +const val SHORT_TIMEOUT_MS = 20L +const val DEFAULT_LINGER_DELAY_MS = 30000 +const val NOT_METERED = NetworkCapabilities.NET_CAPABILITY_NOT_METERED +const val WIFI = NetworkCapabilities.TRANSPORT_WIFI +const val CELLULAR = NetworkCapabilities.TRANSPORT_CELLULAR +const val TEST_INTERFACE_NAME = "testInterfaceName" + +@RunWith(JUnit4::class) +@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs. +class TestableNetworkCallbackTest { + private lateinit var mCallback: TestableNetworkCallback + + private fun makeHasNetwork(netId: Int) = object : TestableNetworkCallback.HasNetwork { + override val network: Network = Network(netId) + } + + @Before + fun setUp() { + mCallback = TestableNetworkCallback() + } + + @Test + fun testLastAvailableNetwork() { + // Make sure there is no last available network at first, then the last available network + // is returned after onAvailable is called. + val net2097 = Network(2097) + assertNull(mCallback.lastAvailableNetwork) + mCallback.onAvailable(net2097) + assertEquals(mCallback.lastAvailableNetwork, net2097) + + // Make sure calling onCapsChanged/onLinkPropertiesChanged don't affect the last available + // network. + mCallback.onCapabilitiesChanged(net2097, NetworkCapabilities()) + mCallback.onLinkPropertiesChanged(net2097, LinkProperties()) + assertEquals(mCallback.lastAvailableNetwork, net2097) + + // Make sure onLost clears the last available network. + mCallback.onLost(net2097) + assertNull(mCallback.lastAvailableNetwork) + + // Do the same but with a different network after onLost : make sure the last available + // network is the new one, not the original one. + val net2098 = Network(2098) + mCallback.onAvailable(net2098) + mCallback.onCapabilitiesChanged(net2098, NetworkCapabilities()) + mCallback.onLinkPropertiesChanged(net2098, LinkProperties()) + assertEquals(mCallback.lastAvailableNetwork, net2098) + + // Make sure onAvailable changes the last available network even if onLost was not called. + val net2099 = Network(2099) + mCallback.onAvailable(net2099) + assertEquals(mCallback.lastAvailableNetwork, net2099) + + // For legacy reasons, lastAvailableNetwork is null as soon as any is lost, not necessarily + // the last available one. Check that behavior. + mCallback.onLost(net2098) + assertNull(mCallback.lastAvailableNetwork) + + // Make sure that losing the really last available one still results in null. + mCallback.onLost(net2099) + assertNull(mCallback.lastAvailableNetwork) + + // Make sure multiple onAvailable in a row then onLost still results in null. + mCallback.onAvailable(net2097) + mCallback.onAvailable(net2098) + mCallback.onAvailable(net2099) + mCallback.onLost(net2097) + assertNull(mCallback.lastAvailableNetwork) + } + + @Test + fun testAssertNoCallback() { + mCallback.assertNoCallback(SHORT_TIMEOUT_MS) + mCallback.onAvailable(Network(100)) + assertFails { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) } + } + + @Test + fun testAssertNoCallbackThat() { + val net = Network(101) + mCallback.assertNoCallbackThat { it is Available } + mCallback.onAvailable(net) + // Expect no blocked status change. Receive other callback does not fail the test. + mCallback.assertNoCallbackThat { it is BlockedStatus } + mCallback.onBlockedStatusChanged(net, true) + assertFails { mCallback.assertNoCallbackThat { it is BlockedStatus } } + mCallback.onBlockedStatusChanged(net, false) + mCallback.onCapabilitiesChanged(net, NetworkCapabilities()) + assertFails { mCallback.assertNoCallbackThat { it is CapabilitiesChanged } } + } + + @Test + fun testCapabilitiesWithAndWithout() { + val net = Network(101) + val matcher = makeHasNetwork(101) + val meteredNc = NetworkCapabilities() + val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED) + // Check that expecting caps (with or without) fails when no callback has been received. + assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + + // Add NOT_METERED and check that With succeeds and Without fails. + mCallback.onCapabilitiesChanged(net, unmeteredNc) + mCallback.expectCapabilitiesWith(NOT_METERED, matcher) + mCallback.onCapabilitiesChanged(net, unmeteredNc) + assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + + // Don't add NOT_METERED and check that With fails and Without succeeds. + mCallback.onCapabilitiesChanged(net, meteredNc) + assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) } + mCallback.onCapabilitiesChanged(net, meteredNc) + mCallback.expectCapabilitiesWithout(NOT_METERED, matcher) + } + + @Test + fun testExpectCallbackThat() { + val net = Network(193) + val netCaps = NetworkCapabilities().addTransportType(CELLULAR) + // Check that expecting callbackThat anything fails when no callback has been received. + assertFails { mCallback.expectCallbackThat(SHORT_TIMEOUT_MS) { true } } + + // Basic test for true and false + mCallback.onAvailable(net) + mCallback.expectCallbackThat { true } + mCallback.onAvailable(net) + assertFails { mCallback.expectCallbackThat(SHORT_TIMEOUT_MS) { false } } + + // Try a positive and a negative case + mCallback.onBlockedStatusChanged(net, true) + mCallback.expectCallbackThat { cb -> cb is BlockedStatus && cb.blocked } + mCallback.onCapabilitiesChanged(net, netCaps) + assertFails { mCallback.expectCallbackThat(SHORT_TIMEOUT_MS) { cb -> + cb is CapabilitiesChanged && cb.caps.hasTransport(WIFI) + } } + } + + @Test + fun testCapabilitiesThat() { + val net = Network(101) + val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI) + // Check that expecting capabilitiesThat anything fails when no callback has been received. + assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { true } } + + // Basic test for true and false + mCallback.onCapabilitiesChanged(net, netCaps) + mCallback.expectCapabilitiesThat(net) { true } + mCallback.onCapabilitiesChanged(net, netCaps) + assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { false } } + + // Try a positive and a negative case + mCallback.onCapabilitiesChanged(net, netCaps) + mCallback.expectCapabilitiesThat(net) { caps -> + caps.hasCapability(NOT_METERED) && + caps.hasTransport(WIFI) && + !caps.hasTransport(CELLULAR) + } + mCallback.onCapabilitiesChanged(net, netCaps) + assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { caps -> + caps.hasTransport(CELLULAR) + } } + + // Try a matching callback on the wrong network + mCallback.onCapabilitiesChanged(net, netCaps) + assertFails { mCallback.expectCapabilitiesThat(Network(100), SHORT_TIMEOUT_MS) { true } } + } + + @Test + fun testLinkPropertiesThat() { + val net = Network(112) + val linkAddress = LinkAddress("fe80::ace:d00d/64") + val mtu = 1984 + val linkProps = LinkProperties().apply { + this.mtu = mtu + interfaceName = TEST_INTERFACE_NAME + addLinkAddress(linkAddress) + } + + // Check that expecting linkPropsThat anything fails when no callback has been received. + assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { true } } + + // Basic test for true and false + mCallback.onLinkPropertiesChanged(net, linkProps) + mCallback.expectLinkPropertiesThat(net) { true } + mCallback.onLinkPropertiesChanged(net, linkProps) + assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { false } } + + // Try a positive and negative case + mCallback.onLinkPropertiesChanged(net, linkProps) + mCallback.expectLinkPropertiesThat(net) { lp -> + lp.interfaceName == TEST_INTERFACE_NAME && + lp.linkAddresses.contains(linkAddress) && + lp.mtu == mtu + } + mCallback.onLinkPropertiesChanged(net, linkProps) + assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { lp -> + lp.interfaceName != TEST_INTERFACE_NAME + } } + + // Try a matching callback on the wrong network + mCallback.onLinkPropertiesChanged(net, linkProps) + assertFails { mCallback.expectLinkPropertiesThat(Network(114), SHORT_TIMEOUT_MS) { lp -> + lp.interfaceName == TEST_INTERFACE_NAME + } } + } + + @Test + fun testExpectCallback() { + val net = Network(103) + // Test expectCallback fails when nothing was sent. + assertFails { mCallback.expectCallback(net, SHORT_TIMEOUT_MS) } + + // Test onAvailable is seen and can be expected + mCallback.onAvailable(net) + mCallback.expectCallback(net, SHORT_TIMEOUT_MS) + + // Test onAvailable won't return calls with a different network + mCallback.onAvailable(Network(106)) + assertFails { mCallback.expectCallback(net, SHORT_TIMEOUT_MS) } + + // Test onAvailable won't return calls with a different callback + mCallback.onAvailable(net) + assertFails { mCallback.expectCallback(net, SHORT_TIMEOUT_MS) } + } + + @Test + fun testPollForNextCallback() { + assertFails { mCallback.pollForNextCallback(SHORT_TIMEOUT_MS) } + TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1, + threadTransform = { cb -> cb.createLinkedCopy() }, spec = """ + sleep; onAvailable(133) | poll(2) = Available(133) time 1..4 + | poll(1) fails + onCapabilitiesChanged(108) | poll(1) = CapabilitiesChanged(108) time 0..3 + onBlockedStatus(199) | poll(1) = BlockedStatus(199) time 0..3 + """) + } + + @Test + fun testEventuallyExpect() { + // TODO: Current test does not verify the inline one. Also verify the behavior after + // aligning two eventuallyExpect() + val net1 = Network(100) + val net2 = Network(101) + mCallback.onAvailable(net1) + mCallback.onCapabilitiesChanged(net1, NetworkCapabilities()) + mCallback.onLinkPropertiesChanged(net1, LinkProperties()) + mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) { + net1.equals(it.network) + } + // No further new callback. Expect no callback. + assertFails { mCallback.eventuallyExpect(LINK_PROPERTIES_CHANGED) } + + // Verify no predicate set. + mCallback.onAvailable(net2) + mCallback.onLinkPropertiesChanged(net2, LinkProperties()) + mCallback.onBlockedStatusChanged(net1, false) + mCallback.eventuallyExpect(BLOCKED_STATUS) { net1.equals(it.network) } + // Verify no callback received if the callback does not happen. + assertFails { mCallback.eventuallyExpect(LOSING) } + } + + @Test + fun testEventuallyExpectOnMultiThreads() { + TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1, + threadTransform = { cb -> cb.createLinkedCopy() }, spec = """ + onAvailable(100) | eventually(CapabilitiesChanged(100), 1) fails + sleep ; onCapabilitiesChanged(100) | eventually(CapabilitiesChanged(100), 2) + onAvailable(101) ; onBlockedStatus(101) | eventually(BlockedStatus(100), 2) fails + onSuspended(100) ; sleep ; onLost(100) | eventually(Lost(100), 2) + """) + } +} + +private object TNCInterpreter : ConcurrentInterpreter(interpretTable) + +val EntryList = CallbackEntry::class.sealedSubclasses.map { it.simpleName }.joinToString("|") +private fun callbackEntryFromString(name: String): KClass { + return CallbackEntry::class.sealedSubclasses.first { it.simpleName == name } +} + +@SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs. +private val interpretTable = listOf>( + // Interpret "Available(xx)" as "call to onAvailable with netId xx", and likewise for + // all callback types. This is implemented above by enumerating the subclasses of + // CallbackEntry and reading their simpleName. + Regex("""(.*)\s+=\s+($EntryList)\((\d+)\)""") to { i, cb, t -> + val record = i.interpret(t.strArg(1), cb) + assertTrue(callbackEntryFromString(t.strArg(2)).isInstance(record)) + // Strictly speaking testing for is CallbackEntry is useless as it's been tested above + // but the compiler can't figure things out from the isInstance call. It does understand + // from the assertTrue(is CallbackEntry) that this is true, which allows to access + // the 'network' member below. + assertTrue(record is CallbackEntry) + assertEquals(record.network.netId, t.intArg(3)) + }, + // Interpret "onAvailable(xx)" as calling "onAvailable" with a netId of xx, and likewise for + // all callback types. NetworkCapabilities and LinkProperties just get an empty object + // as their argument. Losing gets the default linger timer. Blocked gets false. + Regex("""on($EntryList)\((\d+)\)""") to { i, cb, t -> + val net = Network(t.intArg(2)) + when (t.strArg(1)) { + "Available" -> cb.onAvailable(net) + // PreCheck not used in tests. Add it here if it becomes useful. + "CapabilitiesChanged" -> cb.onCapabilitiesChanged(net, NetworkCapabilities()) + "LinkPropertiesChanged" -> cb.onLinkPropertiesChanged(net, LinkProperties()) + "Suspended" -> cb.onNetworkSuspended(net) + "Resumed" -> cb.onNetworkResumed(net) + "Losing" -> cb.onLosing(net, DEFAULT_LINGER_DELAY_MS) + "Lost" -> cb.onLost(net) + "Unavailable" -> cb.onUnavailable() + "BlockedStatus" -> cb.onBlockedStatusChanged(net, false) + else -> fail("Unknown callback type") + } + }, + Regex("""poll\((\d+)\)""") to { i, cb, t -> + cb.pollForNextCallback(t.timeArg(1)) + }, + // Interpret "eventually(Available(xx), timeout)" as calling eventuallyExpect that expects + // CallbackEntry.AVAILABLE with netId of xx within timeout*INTERPRET_TIME_UNIT timeout, and + // likewise for all callback types. + Regex("""eventually\(($EntryList)\((\d+)\),\s+(\d+)\)""") to { i, cb, t -> + val net = Network(t.intArg(2)) + val timeout = t.timeArg(3) + when (t.strArg(1)) { + "Available" -> cb.eventuallyExpect(AVAILABLE, timeout) { net == it.network } + "Suspended" -> cb.eventuallyExpect(SUSPENDED, timeout) { net == it.network } + "Resumed" -> cb.eventuallyExpect(RESUMED, timeout) { net == it.network } + "Losing" -> cb.eventuallyExpect(LOSING, timeout) { net == it.network } + "Lost" -> cb.eventuallyExpect(LOST, timeout) { net == it.network } + "Unavailable" -> cb.eventuallyExpect(UNAVAILABLE, timeout) { net == it.network } + "BlockedStatus" -> cb.eventuallyExpect(BLOCKED_STATUS, timeout) { net == it.network } + "CapabilitiesChanged" -> + cb.eventuallyExpect(NETWORK_CAPS_UPDATED, timeout) { net == it.network } + "LinkPropertiesChanged" -> + cb.eventuallyExpect(LINK_PROPERTIES_CHANGED, timeout) { net == it.network } + else -> fail("Unknown callback type") + } + } +)