Make TestConnectivityManager usable by other tethering tests.

Currently, this class is a static inner class of
UpstreamNetworkMonitorTest. Extract it to its own top-level class
so it can be used by other tests.

Bug: 173068192
Test: atest TetheringTests
Change-Id: I6bdb090a99781ac2530b3924ac5c4cf78de315b0
This commit is contained in:
Lorenzo Colitti 2021-02-28 17:32:30 +09:00
commit d025c562f0
2 changed files with 250 additions and 196 deletions

View file

@ -0,0 +1,247 @@
/*
* Copyright (C) 2021 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.networkstack.tethering;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Handler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts.
*
* TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService.
* Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest.
*
* Things to consider:
* - ConnectivityService uses a real handler for realism, and these test use TestLooper (or even
* invoke callbacks directly inline) for determinism. Using a real ConnectivityService would
* require adding dispatchAll() calls and migrating to handlers.
* - ConnectivityService does not provide a way to order CONNECTIVITY_ACTION before or after the
* NetworkCallbacks for the same network change. That ability is useful because the upstream
* selection code in Tethering is vulnerable to race conditions, due to its reliance on multiple
* separate NetworkCallbacks and BroadcastReceivers, each of which trigger different types of
* updates. If/when the upstream selection code is refactored to a more level-triggered model
* (e.g., with an idempotent function that takes into account all state every time any part of
* that state changes), this may become less important or unnecessary.
*/
public class TestConnectivityManager extends ConnectivityManager {
public Map<NetworkCallback, Handler> allCallbacks = new HashMap<>();
public Set<NetworkCallback> trackingDefault = new HashSet<>();
public TestNetworkAgent defaultNetwork = null;
public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>();
public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>();
private final NetworkRequest mDefaultRequest;
private int mNetworkId = 100;
public TestConnectivityManager(Context ctx, IConnectivityManager svc,
NetworkRequest defaultRequest) {
super(ctx, svc);
mDefaultRequest = defaultRequest;
}
boolean hasNoCallbacks() {
return allCallbacks.isEmpty()
&& trackingDefault.isEmpty()
&& listening.isEmpty()
&& requested.isEmpty()
&& legacyTypeMap.isEmpty();
}
boolean onlyHasDefaultCallbacks() {
return (allCallbacks.size() == 1)
&& (trackingDefault.size() == 1)
&& listening.isEmpty()
&& requested.isEmpty()
&& legacyTypeMap.isEmpty();
}
boolean isListeningForAll() {
final NetworkCapabilities empty = new NetworkCapabilities();
empty.clearAll();
for (NetworkRequest req : listening.values()) {
if (req.networkCapabilities.equalRequestableCapabilities(empty)) {
return true;
}
}
return false;
}
int getNetworkId() {
return ++mNetworkId;
}
void makeDefaultNetwork(TestNetworkAgent agent) {
if (Objects.equals(defaultNetwork, agent)) return;
final TestNetworkAgent formerDefault = defaultNetwork;
defaultNetwork = agent;
for (NetworkCallback cb : trackingDefault) {
if (defaultNetwork != null) {
cb.onAvailable(defaultNetwork.networkId);
cb.onCapabilitiesChanged(
defaultNetwork.networkId, defaultNetwork.networkCapabilities);
cb.onLinkPropertiesChanged(
defaultNetwork.networkId, defaultNetwork.linkProperties);
}
}
}
@Override
public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
assertFalse(allCallbacks.containsKey(cb));
allCallbacks.put(cb, h);
if (mDefaultRequest.equals(req)) {
assertFalse(trackingDefault.contains(cb));
trackingDefault.add(cb);
} else {
assertFalse(requested.containsKey(cb));
requested.put(cb, req);
}
}
@Override
public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
fail("Should never be called.");
}
@Override
public void requestNetwork(NetworkRequest req,
int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
assertFalse(allCallbacks.containsKey(cb));
allCallbacks.put(cb, h);
assertFalse(requested.containsKey(cb));
requested.put(cb, req);
assertFalse(legacyTypeMap.containsKey(cb));
if (legacyType != ConnectivityManager.TYPE_NONE) {
legacyTypeMap.put(cb, legacyType);
}
}
@Override
public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
assertFalse(allCallbacks.containsKey(cb));
allCallbacks.put(cb, h);
assertFalse(listening.containsKey(cb));
listening.put(cb, req);
}
@Override
public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
fail("Should never be called.");
}
@Override
public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
fail("Should never be called.");
}
@Override
public void registerDefaultNetworkCallback(NetworkCallback cb) {
fail("Should never be called.");
}
@Override
public void unregisterNetworkCallback(NetworkCallback cb) {
if (trackingDefault.contains(cb)) {
trackingDefault.remove(cb);
} else if (listening.containsKey(cb)) {
listening.remove(cb);
} else if (requested.containsKey(cb)) {
requested.remove(cb);
legacyTypeMap.remove(cb);
} else {
fail("Unexpected callback removed");
}
allCallbacks.remove(cb);
assertFalse(allCallbacks.containsKey(cb));
assertFalse(trackingDefault.contains(cb));
assertFalse(listening.containsKey(cb));
assertFalse(requested.containsKey(cb));
}
public static class TestNetworkAgent {
public final TestConnectivityManager cm;
public final Network networkId;
public final int transportType;
public final NetworkCapabilities networkCapabilities;
public final LinkProperties linkProperties;
public TestNetworkAgent(TestConnectivityManager cm, int transportType) {
this.cm = cm;
this.networkId = new Network(cm.getNetworkId());
this.transportType = transportType;
networkCapabilities = new NetworkCapabilities();
networkCapabilities.addTransportType(transportType);
networkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
linkProperties = new LinkProperties();
}
public void fakeConnect() {
for (NetworkCallback cb : cm.listening.keySet()) {
cb.onAvailable(networkId);
cb.onCapabilitiesChanged(networkId, copy(networkCapabilities));
cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
}
}
public void fakeDisconnect() {
for (NetworkCallback cb : cm.listening.keySet()) {
cb.onLost(networkId);
}
}
public void sendLinkProperties() {
for (NetworkCallback cb : cm.listening.keySet()) {
cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
}
}
@Override
public String toString() {
return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
}
}
static NetworkCapabilities copy(NetworkCapabilities nc) {
return new NetworkCapabilities(nc);
}
static LinkProperties copy(LinkProperties lp) {
return new LinkProperties(lp);
}
}

View file

@ -29,7 +29,6 @@ import static com.android.networkstack.tethering.UpstreamNetworkMonitor.TYPE_NON
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
@ -48,7 +47,6 @@ import android.net.IConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.util.SharedLog;
@ -60,6 +58,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
import org.junit.After;
import org.junit.Before;
@ -71,10 +70,7 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@RunWith(AndroidJUnit4.class)
@ -106,7 +102,7 @@ public class UpstreamNetworkMonitorTest {
when(mLog.forSubComponent(anyString())).thenReturn(mLog);
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
mCM = spy(new TestConnectivityManager(mContext, mCS));
mCM = spy(new TestConnectivityManager(mContext, mCS, sDefaultRequest));
mSM = new TestStateMachine();
mUNM = new UpstreamNetworkMonitor(
(ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE);
@ -567,187 +563,6 @@ public class UpstreamNetworkMonitorTest {
return false;
}
public static class TestConnectivityManager extends ConnectivityManager {
public Map<NetworkCallback, Handler> allCallbacks = new HashMap<>();
public Set<NetworkCallback> trackingDefault = new HashSet<>();
public TestNetworkAgent defaultNetwork = null;
public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>();
public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>();
private int mNetworkId = 100;
public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
super(ctx, svc);
}
boolean hasNoCallbacks() {
return allCallbacks.isEmpty()
&& trackingDefault.isEmpty()
&& listening.isEmpty()
&& requested.isEmpty()
&& legacyTypeMap.isEmpty();
}
boolean onlyHasDefaultCallbacks() {
return (allCallbacks.size() == 1)
&& (trackingDefault.size() == 1)
&& listening.isEmpty()
&& requested.isEmpty()
&& legacyTypeMap.isEmpty();
}
boolean isListeningForAll() {
final NetworkCapabilities empty = new NetworkCapabilities();
empty.clearAll();
for (NetworkRequest req : listening.values()) {
if (req.networkCapabilities.equalRequestableCapabilities(empty)) {
return true;
}
}
return false;
}
int getNetworkId() {
return ++mNetworkId;
}
void makeDefaultNetwork(TestNetworkAgent agent) {
if (Objects.equals(defaultNetwork, agent)) return;
final TestNetworkAgent formerDefault = defaultNetwork;
defaultNetwork = agent;
for (NetworkCallback cb : trackingDefault) {
if (defaultNetwork != null) {
cb.onAvailable(defaultNetwork.networkId);
cb.onCapabilitiesChanged(
defaultNetwork.networkId, defaultNetwork.networkCapabilities);
cb.onLinkPropertiesChanged(
defaultNetwork.networkId, defaultNetwork.linkProperties);
}
}
}
@Override
public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
assertFalse(allCallbacks.containsKey(cb));
allCallbacks.put(cb, h);
if (sDefaultRequest.equals(req)) {
assertFalse(trackingDefault.contains(cb));
trackingDefault.add(cb);
} else {
assertFalse(requested.containsKey(cb));
requested.put(cb, req);
}
}
@Override
public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
fail("Should never be called.");
}
@Override
public void requestNetwork(NetworkRequest req,
int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
assertFalse(allCallbacks.containsKey(cb));
allCallbacks.put(cb, h);
assertFalse(requested.containsKey(cb));
requested.put(cb, req);
assertFalse(legacyTypeMap.containsKey(cb));
if (legacyType != ConnectivityManager.TYPE_NONE) {
legacyTypeMap.put(cb, legacyType);
}
}
@Override
public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
assertFalse(allCallbacks.containsKey(cb));
allCallbacks.put(cb, h);
assertFalse(listening.containsKey(cb));
listening.put(cb, req);
}
@Override
public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
fail("Should never be called.");
}
@Override
public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
fail("Should never be called.");
}
@Override
public void registerDefaultNetworkCallback(NetworkCallback cb) {
fail("Should never be called.");
}
@Override
public void unregisterNetworkCallback(NetworkCallback cb) {
if (trackingDefault.contains(cb)) {
trackingDefault.remove(cb);
} else if (listening.containsKey(cb)) {
listening.remove(cb);
} else if (requested.containsKey(cb)) {
requested.remove(cb);
legacyTypeMap.remove(cb);
} else {
fail("Unexpected callback removed");
}
allCallbacks.remove(cb);
assertFalse(allCallbacks.containsKey(cb));
assertFalse(trackingDefault.contains(cb));
assertFalse(listening.containsKey(cb));
assertFalse(requested.containsKey(cb));
}
}
public static class TestNetworkAgent {
public final TestConnectivityManager cm;
public final Network networkId;
public final int transportType;
public final NetworkCapabilities networkCapabilities;
public final LinkProperties linkProperties;
public TestNetworkAgent(TestConnectivityManager cm, int transportType) {
this.cm = cm;
this.networkId = new Network(cm.getNetworkId());
this.transportType = transportType;
networkCapabilities = new NetworkCapabilities();
networkCapabilities.addTransportType(transportType);
networkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
linkProperties = new LinkProperties();
}
public void fakeConnect() {
for (NetworkCallback cb : cm.listening.keySet()) {
cb.onAvailable(networkId);
cb.onCapabilitiesChanged(networkId, copy(networkCapabilities));
cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
}
}
public void fakeDisconnect() {
for (NetworkCallback cb : cm.listening.keySet()) {
cb.onLost(networkId);
}
}
public void sendLinkProperties() {
for (NetworkCallback cb : cm.listening.keySet()) {
cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
}
}
@Override
public String toString() {
return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
}
}
public static class TestStateMachine extends StateMachine {
public final ArrayList<Message> messages = new ArrayList<>();
private final State mLoggingState = new LoggingState();
@ -775,14 +590,6 @@ public class UpstreamNetworkMonitorTest {
}
}
static NetworkCapabilities copy(NetworkCapabilities nc) {
return new NetworkCapabilities(nc);
}
static LinkProperties copy(LinkProperties lp) {
return new LinkProperties(lp);
}
static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
final Set<String> expectedSet = new HashSet<>();
Collections.addAll(expectedSet, expected);
@ -797,4 +604,4 @@ public class UpstreamNetworkMonitorTest {
expectation, prefixes.contains(new IpPrefix(expectedPrefix)));
}
}
}
}