From a8d129b1a5458eda4393e3c1e65a81683157fcf4 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 10 Jun 2022 12:19:17 -0700 Subject: [PATCH 1/6] ethernet: get MTU size directly from TestNetworkInterface Test: atest EthernetManagerTest Change-Id: Ic97e3eeb7518d869702247c77061c329eb48b637 --- tests/cts/net/src/android/net/cts/EthernetManagerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt index db24b4493b..e8d7115320 100644 --- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt @@ -122,7 +122,7 @@ class EthernetManagerTest { val tnm = context.getSystemService(TestNetworkManager::class.java) tnm.createTapInterface(false /* bringUp */) } - val mtu = 1500 + val mtu = tapInterface.mtu packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu) raResponder = RouterAdvertisementResponder(packetReader) raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"), From 8b9bea667bcaae83fc5bb4e9eda7e66fb2a21ba5 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 27 May 2022 12:31:33 -0700 Subject: [PATCH 2/6] ethernet: address test review comments Addresses open comments from aosp/2101321. Test: atest EthernetManagerTest Bug: 197548738 Change-Id: Ide697f65a2be977212ecdbdf753645b84b1d7634 --- .../android/net/cts/EthernetManagerTest.kt | 128 ++++++++++-------- 1 file changed, 72 insertions(+), 56 deletions(-) diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt index e8d7115320..e5a06c7acb 100644 --- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt @@ -104,7 +104,7 @@ class EthernetManagerTest { private val ifaceListener = EthernetStateListener() private val createdIfaces = ArrayList() private val addedListeners = ArrayList() - private val networkRequests = ArrayList() + private val registeredCallbacks = ArrayList() private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null @@ -115,7 +115,7 @@ class EthernetManagerTest { private val tapInterface: TestNetworkInterface private val packetReader: TapPacketReader private val raResponder: RouterAdvertisementResponder - val interfaceName get() = tapInterface.interfaceName + val name get() = tapInterface.interfaceName init { tapInterface = runAsShell(MANAGE_TEST_NETWORKS) { @@ -172,7 +172,7 @@ class EthernetManagerTest { } fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) { - expectCallback(createChangeEvent(iface.interfaceName, state, role)) + expectCallback(createChangeEvent(iface.name, state, role)) } fun createChangeEvent(iface: String, state: Int, role: Int) = @@ -190,7 +190,7 @@ class EthernetManagerTest { } fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) { - eventuallyExpect(iface.interfaceName, state, role) + eventuallyExpect(iface.name, state, role) } fun assertNoCallback() { @@ -243,7 +243,7 @@ class EthernetManagerTest { for (listener in addedListeners) { em.removeInterfaceStateListener(listener) } - networkRequests.forEach { cm.unregisterNetworkCallback(it) } + registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) } releaseTetheredInterface() } @@ -282,13 +282,15 @@ class EthernetManagerTest { private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback { return TestableNetworkCallback().also { cm.requestNetwork(request, it) - networkRequests.add(it) + registeredCallbacks.add(it) } } - private fun releaseNetwork(cb: TestableNetworkCallback) { - cm.unregisterNetworkCallback(cb) - networkRequests.remove(cb) + private fun registerNetworkListener(request: NetworkRequest): TestableNetworkCallback { + return TestableNetworkCallback().also { + cm.registerNetworkCallback(request, it) + registeredCallbacks.add(it) + } } private fun requestTetheredInterface() = TetheredInterfaceListener().also { @@ -304,22 +306,34 @@ class EthernetManagerTest { } } + private fun releaseRequest(cb: TestableNetworkCallback) { + cm.unregisterNetworkCallback(cb) + registeredCallbacks.remove(cb) + } + + // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to + // keep ETH_REQUEST as it is, a defensive copy is created here. private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) = NetworkRequest.Builder(NetworkRequest(ETH_REQUEST)) .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build() // It can take multiple seconds for the network to become available. private fun TestableNetworkCallback.expectAvailable() = - expectCallback(anyNetwork(), 5000/*ms timeout*/).network + expectCallback(anyNetwork(), 5000 /* ms timeout */).network // b/233534110: eventuallyExpect() does not advance ReadHead, use // eventuallyExpect(Lost::class) instead. private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) = eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true } - private fun TestableNetworkCallback.assertNotLost(n: Network? = null) = + private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) = assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) } + private fun TestableNetworkCallback.expectCapabilitiesWithInterfaceName(name: String) = + expectCapabilitiesThat(anyNetwork()) { + it.networkSpecifier == EthernetNetworkSpecifier(name) + } + @Test fun testCallbacks() { // If an interface exists when the callback is registered, it is reported on registration. @@ -398,11 +412,8 @@ class EthernetManagerTest { assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned") assertTrue(ifaces.contains(iface), "Untracked interface $iface returned") // If the event's iface was created in the test, additional criteria can be validated. - createdIfaces.find { it.interfaceName.equals(iface) }?.let { - assertEquals(event, - listener.createChangeEvent(it.interfaceName, - STATE_LINK_UP, - ROLE_CLIENT)) + createdIfaces.find { it.name.equals(iface) }?.let { + assertEquals(event, listener.createChangeEvent(it.name, STATE_LINK_UP, ROLE_CLIENT)) } } // Assert all callbacks are accounted for. @@ -411,87 +422,75 @@ class EthernetManagerTest { @Test fun testGetInterfaceList() { - setIncludeTestInterfaces(true) - // Create two test interfaces and check the return list contains the interface names. val iface1 = createInterface() val iface2 = createInterface() var ifaces = em.getInterfaceList() assertTrue(ifaces.size > 0) - assertTrue(ifaces.contains(iface1.interfaceName)) - assertTrue(ifaces.contains(iface2.interfaceName)) + assertTrue(ifaces.contains(iface1.name)) + assertTrue(ifaces.contains(iface2.name)) // Remove one existing test interface and check the return list doesn't contain the // removed interface name. removeInterface(iface1) ifaces = em.getInterfaceList() - assertFalse(ifaces.contains(iface1.interfaceName)) - assertTrue(ifaces.contains(iface2.interfaceName)) + assertFalse(ifaces.contains(iface1.name)) + assertTrue(ifaces.contains(iface2.name)) removeInterface(iface2) } @Test fun testNetworkRequest_withSingleExistingInterface() { - setIncludeTestInterfaces(true) createInterface() // install a listener which will later be used to verify the Lost callback - val listenerCb = TestableNetworkCallback() - cm.registerNetworkCallback(ETH_REQUEST, listenerCb) - networkRequests.add(listenerCb) + val listenerCb = registerNetworkListener(ETH_REQUEST) val cb = requestNetwork(ETH_REQUEST) val network = cb.expectAvailable() - cb.assertNotLost() - releaseNetwork(cb) + cb.assertNeverLost() + releaseRequest(cb) listenerCb.eventuallyExpectLost(network) } @Test fun testNetworkRequest_beforeSingleInterfaceIsUp() { - setIncludeTestInterfaces(true) - val cb = requestNetwork(ETH_REQUEST) - // bring up interface after network has been requested + // bring up interface after network has been requested. + // Note: there is no guarantee that the NetworkRequest has been processed before the + // interface is actually created. That being said, it takes a few seconds between calling + // createInterface and the interface actually being properly registered with the ethernet + // module, so it is extremely unlikely that the CS handler thread has not run until then. val iface = createInterface() val network = cb.expectAvailable() // remove interface before network request has been removed - cb.assertNotLost() + cb.assertNeverLost() removeInterface(iface) cb.eventuallyExpectLost() - - releaseNetwork(cb) } @Test fun testNetworkRequest_withMultipleInterfaces() { - setIncludeTestInterfaces(true) - val iface1 = createInterface() val iface2 = createInterface() - val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName)) + val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name)) val network = cb.expectAvailable() - cb.expectCapabilitiesThat(network) { - it.networkSpecifier == EthernetNetworkSpecifier(iface2.interfaceName) - } + cb.expectCapabilitiesWithInterfaceName(iface2.name) removeInterface(iface1) - cb.assertNotLost() + cb.assertNeverLost() removeInterface(iface2) cb.eventuallyExpectLost() - - releaseNetwork(cb) } @Test fun testNetworkRequest_withInterfaceBeingReplaced() { - setIncludeTestInterfaces(true) val iface1 = createInterface() val cb = requestNetwork(ETH_REQUEST) @@ -499,36 +498,53 @@ class EthernetManagerTest { // create another network and verify the request sticks to the current network val iface2 = createInterface() - cb.assertNotLost() + cb.assertNeverLost() // remove iface1 and verify the request brings up iface2 removeInterface(iface1) cb.eventuallyExpectLost(network) val network2 = cb.expectAvailable() - - releaseNetwork(cb) } @Test fun testNetworkRequest_withMultipleInterfacesAndRequests() { - setIncludeTestInterfaces(true) val iface1 = createInterface() val iface2 = createInterface() - val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.interfaceName)) - val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName)) + val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.name)) + val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name)) val cb3 = requestNetwork(ETH_REQUEST) cb1.expectAvailable() + cb1.expectCapabilitiesWithInterfaceName(iface1.name) cb2.expectAvailable() + cb2.expectCapabilitiesWithInterfaceName(iface2.name) + // this request can be matched by either network. cb3.expectAvailable() - cb1.assertNotLost() - cb2.assertNotLost() - cb3.assertNotLost() + cb1.assertNeverLost() + cb2.assertNeverLost() + cb3.assertNeverLost() + } - releaseNetwork(cb1) - releaseNetwork(cb2) - releaseNetwork(cb3) + @Test + fun testNetworkRequest_ensureProperRefcounting() { + // create first request before interface is up / exists; create another request after it has + // been created; release one of them and check that the network stays up. + val listener = registerNetworkListener(ETH_REQUEST) + val cb1 = requestNetwork(ETH_REQUEST) + + val iface = createInterface() + val network = cb1.expectAvailable() + + val cb2 = requestNetwork(ETH_REQUEST) + cb2.expectAvailable() + + // release the first request; this used to trigger b/197548738 + releaseRequest(cb1) + + cb2.assertNeverLost() + releaseRequest(cb2) + listener.eventuallyExpectLost(network) } } From 7083beb088f34c03430c9eb55ce42350e6bfe7fe Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Wed, 8 Jun 2022 15:32:08 -0700 Subject: [PATCH 3/6] ethernet: clean up HandlerExecutor usage in test Test: atest EthernetManagerTest Change-Id: I290136b4628e6ee7756772faa477467b04aade2e --- tests/cts/net/src/android/net/cts/EthernetManagerTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt index e5a06c7acb..5af679fc24 100644 --- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt @@ -44,7 +44,6 @@ import android.net.TestNetworkManager import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged import android.os.Build import android.os.Handler -import android.os.HandlerExecutor import android.os.Looper import android.os.SystemProperties import android.platform.test.annotations.AppModeFull @@ -100,6 +99,7 @@ class EthernetManagerTest { private val context by lazy { InstrumentationRegistry.getInstrumentation().context } private val em by lazy { context.getSystemService(EthernetManager::class.java) } private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) } + private val handler by lazy { Handler(Looper.getMainLooper()) } private val ifaceListener = EthernetStateListener() private val createdIfaces = ArrayList() @@ -249,7 +249,7 @@ class EthernetManagerTest { private fun addInterfaceStateListener(listener: EthernetStateListener) { runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) { - em.addInterfaceStateListener(HandlerExecutor(Handler(Looper.getMainLooper())), listener) + em.addInterfaceStateListener(handler::post, listener) } addedListeners.add(listener) } @@ -257,7 +257,7 @@ class EthernetManagerTest { private fun createInterface(): EthernetTestInterface { val iface = EthernetTestInterface( context, - Handler(Looper.getMainLooper()) + handler, ).also { createdIfaces.add(it) } with(ifaceListener) { // when an interface comes up, we should always see a down cb before an up cb. @@ -295,7 +295,7 @@ class EthernetManagerTest { private fun requestTetheredInterface() = TetheredInterfaceListener().also { tetheredInterfaceRequest = runAsShell(NETWORK_SETTINGS) { - em.requestTetheredInterface(HandlerExecutor(Handler(Looper.getMainLooper())), it) + em.requestTetheredInterface(handler::post, it) } } From 901ff962102e57f66ec8a058cfa240faabc1d778 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Thu, 16 Jun 2022 10:53:42 -0700 Subject: [PATCH 4/6] ethernet: add support for enable/disableInterface in tests Adds an outcome receiver and wraps the EthernetManager#enableInterface and EthernetManager#disableInterface APIs. Test: m -- test only change Change-Id: I7064edda3ebbdd91e2bf28651fdf85b9a7cf2134 --- .../android/net/cts/EthernetManagerTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt index 5af679fc24..0a1dccf486 100644 --- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt @@ -30,6 +30,7 @@ import android.net.EthernetManager.STATE_LINK_DOWN import android.net.EthernetManager.STATE_LINK_UP import android.net.EthernetManager.TetheredInterfaceCallback import android.net.EthernetManager.TetheredInterfaceRequest +import android.net.EthernetNetworkManagementException import android.net.EthernetNetworkSpecifier import android.net.InetAddresses import android.net.IpConfiguration @@ -45,6 +46,7 @@ import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.I import android.os.Build import android.os.Handler import android.os.Looper +import android.os.OutcomeReceiver import android.os.SystemProperties import android.platform.test.annotations.AppModeFull import android.util.ArraySet @@ -227,6 +229,35 @@ class EthernetManagerTest { } } + private class EthernetOutcomeReceiver : + OutcomeReceiver { + private val result = CompletableFuture() + + override fun onResult(iface: String) { + result.complete(iface) + } + + override fun onError(e: EthernetNetworkManagementException) { + result.completeExceptionally(e) + } + + fun expectResult(expected: String) { + assertEquals(expected, result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)) + } + + fun expectError() { + // Assert that the future fails with EthernetNetworkManagementException from the + // completeExceptionally() call inside onUnavailable. + assertFailsWith(EthernetNetworkManagementException::class) { + try { + result.get() + } catch (e: ExecutionException) { + throw e.cause!! + } + } + } + } + @Before fun setUp() { setIncludeTestInterfaces(true) @@ -311,6 +342,18 @@ class EthernetManagerTest { registeredCallbacks.remove(cb) } + private fun disableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also { + runAsShell(MANAGE_TEST_NETWORKS) { + em.disableInterface(iface.name, handler::post, it) + } + } + + private fun enableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also { + runAsShell(MANAGE_TEST_NETWORKS) { + em.enableInterface(iface.name, handler::post, it) + } + } + // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to // keep ETH_REQUEST as it is, a defensive copy is created here. private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) = From 7312d3e0d61cffa166190ce13d32af6bb5196b50 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Thu, 16 Jun 2022 12:10:54 -0700 Subject: [PATCH 5/6] ethernet: add updateConfiguration support to EthernetManagerTest Test: m -- test only change Change-Id: Ie97c2611b45d97ca504e6b536ae957d442b314ae --- .../src/android/net/cts/EthernetManagerTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt index 0a1dccf486..e3eb0b3b32 100644 --- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt @@ -32,10 +32,12 @@ import android.net.EthernetManager.TetheredInterfaceCallback import android.net.EthernetManager.TetheredInterfaceRequest import android.net.EthernetNetworkManagementException import android.net.EthernetNetworkSpecifier +import android.net.EthernetNetworkUpdateRequest import android.net.InetAddresses import android.net.IpConfiguration import android.net.MacAddress import android.net.Network +import android.net.NetworkCapabilities import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_TEST @@ -354,6 +356,22 @@ class EthernetManagerTest { } } + private fun updateConfiguration( + iface: EthernetTestInterface, + ipConfig: IpConfiguration? = null, + capabilities: NetworkCapabilities? = null + ) = EthernetOutcomeReceiver().also { + runAsShell(MANAGE_TEST_NETWORKS) { + em.updateConfiguration( + iface.name, + EthernetNetworkUpdateRequest.Builder() + .setIpConfiguration(ipConfig) + .setNetworkCapabilities(capabilities).build(), + handler::post, + it) + } + } + // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to // keep ETH_REQUEST as it is, a defensive copy is created here. private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) = From 5cfb761d6c7f56fb2be3bf32f78dba8ad32a49a3 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Wed, 8 Jun 2022 11:15:45 -0700 Subject: [PATCH 6/6] ethernet: add test that covers link up / down behavior Test: atest EthernetManagerTest Change-Id: I703848309ffbe3993bdcd9666def4a64039f905f --- .../android/net/cts/EthernetManagerTest.kt | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt index e3eb0b3b32..d0d44dcc47 100644 --- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt @@ -114,17 +114,21 @@ class EthernetManagerTest { private class EthernetTestInterface( context: Context, - private val handler: Handler + private val handler: Handler, + hasCarrier: Boolean ) { private val tapInterface: TestNetworkInterface private val packetReader: TapPacketReader private val raResponder: RouterAdvertisementResponder + private val tnm: TestNetworkManager val name get() = tapInterface.interfaceName init { + tnm = runAsShell(MANAGE_TEST_NETWORKS) { + context.getSystemService(TestNetworkManager::class.java) + } tapInterface = runAsShell(MANAGE_TEST_NETWORKS) { - val tnm = context.getSystemService(TestNetworkManager::class.java) - tnm.createTapInterface(false /* bringUp */) + tnm.createTapInterface(hasCarrier, false /* bringUp */) } val mtu = tapInterface.mtu packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu) @@ -136,6 +140,12 @@ class EthernetManagerTest { raResponder.start() } + fun setCarrierEnabled(enabled: Boolean) { + runAsShell(MANAGE_TEST_NETWORKS) { + tnm.setCarrierEnabled(tapInterface, enabled) + } + } + fun destroy() { raResponder.stop() handler.post({ packetReader.stop() }) @@ -287,15 +297,17 @@ class EthernetManagerTest { addedListeners.add(listener) } - private fun createInterface(): EthernetTestInterface { + private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface { val iface = EthernetTestInterface( context, handler, + hasCarrier ).also { createdIfaces.add(it) } - with(ifaceListener) { - // when an interface comes up, we should always see a down cb before an up cb. - eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT) - expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT) + + // when an interface comes up, we should always see a down cb before an up cb. + ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT) + if (hasCarrier) { + ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT) } return iface } @@ -390,6 +402,9 @@ class EthernetManagerTest { private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) = assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) } + private fun TestableNetworkCallback.assertNeverAvailable(n: Network? = null) = + assertNoCallbackThat() { it is Available && (n?.equals(it.network) ?: true) } + private fun TestableNetworkCallback.expectCapabilitiesWithInterfaceName(name: String) = expectCapabilitiesThat(anyNetwork()) { it.networkSpecifier == EthernetNetworkSpecifier(name) @@ -608,4 +623,18 @@ class EthernetManagerTest { releaseRequest(cb2) listener.eventuallyExpectLost(network) } + + @Test + fun testNetworkRequest_forInterfaceWhileTogglingCarrier() { + val iface = createInterface(false /* hasCarrier */) + + val cb = requestNetwork(ETH_REQUEST) + cb.assertNeverAvailable() + + iface.setCarrierEnabled(true) + cb.expectAvailable() + + iface.setCarrierEnabled(false) + cb.eventuallyExpectLost() + } }