From 3b6bb1ba4a2ebed1237e40eed45dfdb0818375af Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Tue, 5 Nov 2024 18:36:08 +0800 Subject: [PATCH] [Thread] add integration tests for aosp/3334156 Bug: 374037595 Change-Id: I1a380067681297c6047d9b0b772c873ecd110754 --- .../android/net/thread/BorderRoutingTest.java | 2 +- .../net/thread/ThreadIntegrationTest.java | 18 ++++ .../utils/ThreadNetworkControllerWrapper.java | 13 +++ .../net/thread/utils/ThreadStateListener.java | 96 +++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java index f022187332..ddbef4709e 100644 --- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java +++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java @@ -18,8 +18,8 @@ package android.net.thread; import static android.Manifest.permission.MANAGE_TEST_NETWORKS; import static android.net.InetAddresses.parseNumericAddress; -import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply; import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET; +import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply; import static android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork; import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses; import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet; diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java index 56134545ff..d41550b89c 100644 --- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java +++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java @@ -29,6 +29,7 @@ import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetDa import static android.net.thread.utils.IntegrationTestUtils.getThreadNetwork; import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup; import static android.net.thread.utils.IntegrationTestUtils.waitFor; +import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT; import static com.android.compatibility.common.util.SystemUtil.runShellCommand; import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; @@ -55,6 +56,7 @@ import android.net.thread.utils.OtDaemonController; import android.net.thread.utils.ThreadFeatureCheckerRule; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature; import android.net.thread.utils.ThreadNetworkControllerWrapper; +import android.net.thread.utils.ThreadStateListener; import android.os.SystemClock; import androidx.test.core.app.ApplicationProvider; @@ -138,6 +140,8 @@ public class ThreadIntegrationTest { @After public void tearDown() throws Exception { + ThreadStateListener.stopAllListeners(); + mController.setTestNetworkAsUpstreamAndWait(null); mController.leaveAndWait(); mController.setConfigurationAndWait(DEFAULT_CONFIG); @@ -267,6 +271,20 @@ public class ThreadIntegrationTest { assertTunInterfaceMemberOfGroup(GROUP_ADDR_ALL_ROUTERS); } + @Test + public void joinNetwork_joinTheSameNetworkTwice_neverDetached() throws Exception { + mController.joinAndWait(DEFAULT_DATASET); + mController.waitForRole(DEVICE_ROLE_LEADER, JOIN_TIMEOUT); + + var listener = ThreadStateListener.startListener(mController.get()); + mController.joinAndWait(DEFAULT_DATASET); + + assertThat( + listener.pollForAnyRoleOf( + List.of(DEVICE_ROLE_DETACHED, DEVICE_ROLE_STOPPED), JOIN_TIMEOUT)) + .isNull(); + } + @Test public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception { mController.joinAndWait(DEFAULT_DATASET); diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java index 4354702bfe..b6114f3ec4 100644 --- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java +++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java @@ -37,6 +37,7 @@ import android.net.thread.ThreadNetworkManager; import android.os.OutcomeReceiver; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -53,6 +54,9 @@ public final class ThreadNetworkControllerWrapper { private final ThreadNetworkController mController; + private final List mDeviceRoleUpdates = new ArrayList<>(); + @Nullable private StateCallback mStateCallback; + /** * Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread * feature is not supported on this device. @@ -70,6 +74,15 @@ public final class ThreadNetworkControllerWrapper { mController = controller; } + /** + * Returns the underlying {@link ThreadNetworkController} object or {@code null} if the current + * platform doesn't support it. + */ + @Nullable + public ThreadNetworkController get() { + return mController; + } + /** * Returns the Thread enabled state. * diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java b/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java new file mode 100644 index 0000000000..21eb7d9b39 --- /dev/null +++ b/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java @@ -0,0 +1,96 @@ +/* + * 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.ACCESS_NETWORK_STATE; + +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import android.annotation.Nullable; +import android.net.thread.ThreadNetworkController; +import android.net.thread.ThreadNetworkController.StateCallback; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.ArrayTrackRecord; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * A listener for sequential Thread state updates. + * + *

This is a wrapper around {@link ThreadNetworkController#registerStateCallback} to make + * synchronized access to Thread state updates easier. + */ +@VisibleForTesting +public final class ThreadStateListener { + private static final List sListeners = new ArrayList<>(); + private final ArrayTrackRecord mDeviceRoleUpdates = new ArrayTrackRecord<>(); + private final ArrayTrackRecord.ReadHead mReadHead = mDeviceRoleUpdates.newReadHead(); + private final ThreadNetworkController mController; + private final StateCallback mCallback = + new ThreadNetworkController.StateCallback() { + @Override + public void onDeviceRoleChanged(int newRole) { + mDeviceRoleUpdates.add(newRole); + } + // Add more state update trackers here + }; + + /** Creates a new {@link ThreadStateListener} object and starts listening for state updates. */ + public static ThreadStateListener startListener(ThreadNetworkController controller) { + var listener = new ThreadStateListener(controller); + sListeners.add(listener); + listener.start(); + return listener; + } + + /** Stops all listeners created by {@link #startListener}. */ + public static void stopAllListeners() { + for (var listener : sListeners) { + listener.stop(); + } + sListeners.clear(); + } + + private ThreadStateListener(ThreadNetworkController controller) { + mController = controller; + } + + private void start() { + runAsShell( + ACCESS_NETWORK_STATE, + () -> mController.registerStateCallback(directExecutor(), mCallback)); + } + + private void stop() { + runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback)); + } + + /** + * Polls for any role in {@code roles} starting after call to {@link #startListener}. + * + *

Returns the matched device role or {@code null} if timeout. + */ + @Nullable + public Integer pollForAnyRoleOf(List roles, Duration timeout) { + return mReadHead.poll(timeout.toMillis(), newRole -> (roles.contains(newRole))); + } +}