Merge "Use a TAP test network for MeshCoP service test cases" into main
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 676eb0e..c1cf0a0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -42,6 +42,7 @@
"guava",
"guava-android-testlib",
"net-tests-utils",
+ "ThreadNetworkTestUtils",
"truth",
],
libs: [
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 3bec36b..9549656 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -17,7 +17,6 @@
package android.net.thread.cts;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
@@ -33,7 +32,6 @@
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
@@ -48,7 +46,6 @@
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.LinkAddress;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
@@ -62,7 +59,9 @@
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
+import android.net.thread.utils.TapTestNetworkTracker;
import android.os.Build;
+import android.os.HandlerThread;
import android.os.OutcomeReceiver;
import androidx.annotation.NonNull;
@@ -74,7 +73,6 @@
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
-import com.android.testutils.TestNetworkTracker;
import org.junit.After;
import org.junit.Before;
@@ -110,7 +108,7 @@
private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
- private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 10 * 1000;
+ private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@@ -123,6 +121,8 @@
private NsdManager mNsdManager;
private Set<String> mGrantedPermissions;
+ private HandlerThread mHandlerThread;
+ private TapTestNetworkTracker mTestNetworkTracker;
@Before
public void setUp() throws Exception {
@@ -141,6 +141,8 @@
setEnabledAndWait(mController, true);
mNsdManager = mContext.getSystemService(NsdManager.class);
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
}
@After
@@ -152,6 +154,7 @@
future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
}
dropAllPermissions();
+ tearDownTestNetwork();
}
@Test
@@ -829,7 +832,7 @@
@Test
public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
- TestNetworkTracker testNetwork = setUpTestNetwork();
+ setUpTestNetwork();
setEnabledAndWait(mController, true);
leaveAndWait(mController);
@@ -845,13 +848,11 @@
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
-
- tearDownTestNetwork(testNetwork);
}
@Test
public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
- TestNetworkTracker testNetwork = setUpTestNetwork();
+ setUpTestNetwork();
String networkName = "TestNet" + new Random().nextInt(10_000);
joinRandomizedDatasetAndWait(mController, networkName);
@@ -872,27 +873,26 @@
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
assertThat(txtMap.get("id").length).isEqualTo(16);
-
- tearDownTestNetwork(testNetwork);
}
@Test
public void meshcopService_threadDisabled_notDiscovered() throws Exception {
- TestNetworkTracker testNetwork = setUpTestNetwork();
+ setUpTestNetwork();
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
setEnabledAndWait(mController, false);
-
try {
- serviceLostFuture.get(10_000, MILLISECONDS);
+ serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException ignored) {
+ // It's fine if the service lost event didn't show up. The service may not ever be
+ // advertised.
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
- assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
- tearDownTestNetwork(testNetwork);
+ assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
}
private static void dropAllPermissions() {
@@ -1163,14 +1163,17 @@
}
}
- TestNetworkTracker setUpTestNetwork() {
- return runAsShell(
- MANAGE_TEST_NETWORKS,
- () -> initTestNetwork(mContext, new LinkAddress("2001:db8:123::/64"), 10_000));
+ private void setUpTestNetwork() {
+ assertThat(mTestNetworkTracker).isNull();
+ mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
}
- void tearDownTestNetwork(TestNetworkTracker testNetwork) {
- runAsShell(MANAGE_TEST_NETWORKS, () -> testNetwork.teardown());
+ private void tearDownTestNetwork() throws InterruptedException {
+ if (mTestNetworkTracker != null) {
+ mTestNetworkTracker.tearDown();
+ }
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
}
private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 4948c22..60a5f2b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,7 +16,6 @@
package com.android.server.thread;
-import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
new file mode 100644
index 0000000..24e9bb9
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2023 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 {
+ default_team: "trendy_team_fwk_thread_network",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "ThreadNetworkTestUtils",
+ min_sdk_version: "30",
+ static_libs: [
+ "compatibility-device-util-axt",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ defaults: [
+ "framework-connectivity-test-defaults",
+ ],
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
new file mode 100644
index 0000000..43f177d
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2024 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 android.net.thread.utils;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.testutils.TestableNetworkAgent;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A class that can create/destroy a test network based on TAP interface. */
+public final class TapTestNetworkTracker {
+ private static final Duration TIMEOUT = Duration.ofSeconds(2);
+ private final Context mContext;
+ private final Looper mLooper;
+ private TestNetworkInterface mInterface;
+ private TestableNetworkAgent mAgent;
+ private final TestableNetworkCallback mNetworkCallback;
+ private final ConnectivityManager mConnectivityManager;
+
+ /**
+ * Constructs a {@link TapTestNetworkTracker}.
+ *
+ * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that
+ * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so
+ * the test network won't be automatically turned down by {@link
+ * com.android.server.ConnectivityService}.
+ */
+ public TapTestNetworkTracker(Context context, Looper looper) {
+ mContext = context;
+ mLooper = looper;
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ mNetworkCallback = new TestableNetworkCallback();
+ runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork);
+ }
+
+ /** Tears down the test network. */
+ public void tearDown() {
+ runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork);
+ }
+
+ /** Returns the interface name of the test network. */
+ public String getInterfaceName() {
+ return mInterface.getInterfaceName();
+ }
+
+ private void setUpTestNetwork() throws Exception {
+ mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
+
+ mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(getInterfaceName());
+ mAgent =
+ new TestableNetworkAgent(
+ mContext,
+ mLooper,
+ newNetworkCapabilities(),
+ lp,
+ new NetworkAgentConfig.Builder().build());
+ final Network network = mAgent.register();
+ mAgent.markConnected();
+
+ PollingCheck.check(
+ "No usable address on interface",
+ TIMEOUT.toMillis(),
+ () -> hasUsableAddress(network, getInterfaceName()));
+
+ lp.setLinkAddresses(makeLinkAddresses());
+ mAgent.sendLinkProperties(lp);
+ mNetworkCallback.eventuallyExpect(
+ LINK_PROPERTIES_CHANGED,
+ TIMEOUT.toMillis(),
+ l -> !l.getLp().getAddresses().isEmpty());
+ }
+
+ private void tearDownTestNetwork() throws IOException {
+ mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+ mAgent.unregister();
+ mInterface.getFileDescriptor().close();
+ mAgent.waitForIdle(TIMEOUT.toMillis());
+ }
+
+ private NetworkRequest newNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
+ .build();
+ }
+
+ private NetworkCapabilities newNetworkCapabilities() {
+ return new NetworkCapabilities()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
+ }
+
+ private List<LinkAddress> makeLinkAddresses() {
+ List<LinkAddress> linkAddresses = new ArrayList<>();
+ List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
+
+ try {
+ interfaceAddresses =
+ NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
+ } catch (SocketException ignored) {
+ // Ignore failures when getting the addresses.
+ }
+
+ for (InterfaceAddress address : interfaceAddresses) {
+ linkAddresses.add(
+ new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
+ }
+
+ return linkAddresses;
+ }
+
+ private static boolean hasUsableAddress(Network network, String interfaceName) {
+ try {
+ if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
+ return false;
+ }
+ } catch (SocketException e) {
+ return false;
+ }
+ // Check if the link-local address can be used. Address flags are not available without
+ // elevated permissions, so check that bindSocket works.
+ try {
+ FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ network.bindSocket(sock);
+ Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
+ Os.close(sock);
+ } catch (ErrnoException | IOException e) {
+ return false;
+ }
+ return true;
+ }
+}