Merge "[Thread] add service unit test skeleton" into main am: 3deb5da6db

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2888486

Change-Id: I92124c04db4e4d0ac432fa3fac5adbf8cfb57538
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Kangping Dong 2024-01-03 10:31:32 +00:00 • committed by Automerger Merge Worker
commit 498be80d2e
4 changed files with 225 additions and 40 deletions

View file

@ -36,7 +36,6 @@ import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED
import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
import static android.net.thread.ThreadNetworkException.ErrorCode;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
@ -84,6 +83,7 @@ import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
import android.net.thread.ThreadNetworkException.ErrorCode;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@ -120,8 +120,8 @@ import java.util.function.Supplier;
*
* <p>Threading model: This class is not Thread-safe and should only be accessed from the
* ThreadNetworkService class. Additional attention should be paid to handle the threading code
* correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
* `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
* correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from the
* thread of `mHandler` 2. In the @Override methods, the actual work MUST be dispatched to the
* HandlerThread except for arguments or permissions checking
*/
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@ -133,11 +133,10 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private final Context mContext;
private final Handler mHandler;
// Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
// Below member fields can only be accessed from the handler thread (`mHandler`). In
// particular, the constructor does not run on the handler thread, so it must not touch any of
// the non-final fields, nor must it mutate any of the non-final fields inside these objects.
private final HandlerThread mHandlerThread;
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
@ -149,8 +148,10 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
// TODO(b/308310823): read supported channel from Thread dameon
private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
private IOtDaemon mOtDaemon;
private NetworkAgent mNetworkAgent;
@Nullable private IOtDaemon mOtDaemon;
@Nullable private NetworkAgent mNetworkAgent;
@Nullable private NetworkAgent mTestNetworkAgent;
private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private Network mUpstreamNetwork;
@ -164,15 +165,14 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
@VisibleForTesting
ThreadNetworkControllerService(
Context context,
HandlerThread handlerThread,
Handler handler,
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController) {
mContext = context;
mHandlerThread = handlerThread;
mHandler = new Handler(handlerThread.getLooper());
mHandler = handler;
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
@ -191,7 +191,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
return new ThreadNetworkControllerService(
context,
handlerThread,
new Handler(handlerThread.getLooper()),
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
@ -199,14 +199,6 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
new InfraInterfaceController());
}
private static NetworkCapabilities newNetworkCapabilities() {
return new NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.build();
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
try {
return (Inet6Address) Inet6Address.getByAddress(addressBytes);
@ -302,6 +294,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
private IOtDaemon getOtDaemon() throws RemoteException {
checkOnHandlerThread();
if (mOtDaemon != null) {
return mOtDaemon;
}
@ -310,9 +304,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
otDaemon.initialize(mTunIfController.getTunFd());
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
return mOtDaemon;
}
@ -427,27 +421,47 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mHandler);
}
/** Injects a {@link NetworkAgent} for testing. */
@VisibleForTesting
void setTestNetworkAgent(@Nullable NetworkAgent testNetworkAgent) {
mTestNetworkAgent = testNetworkAgent;
}
private NetworkAgent newNetworkAgent() {
if (mTestNetworkAgent != null) {
return mTestNetworkAgent;
}
final NetworkCapabilities netCaps =
new NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.build();
final NetworkScore score =
new NetworkScore.Builder()
.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
.build();
return new NetworkAgent(
mContext,
mHandler.getLooper(),
TAG,
netCaps,
mLinkProperties,
newLocalNetworkConfig(),
score,
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {};
}
private void registerThreadNetwork() {
if (mNetworkAgent != null) {
return;
}
NetworkCapabilities netCaps = newNetworkCapabilities();
NetworkScore score =
new NetworkScore.Builder()
.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
.build();
requestThreadNetwork();
mNetworkAgent =
new NetworkAgent(
mContext,
mHandlerThread.getLooper(),
TAG,
netCaps,
mLinkProperties,
newLocalNetworkConfig(),
score,
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {};
mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
Log.i(TAG, "Registered Thread network");
@ -634,7 +648,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
private void checkOnHandlerThread() {
if (Looper.myLooper() != mHandlerThread.getLooper()) {
if (Looper.myLooper() != mHandler.getLooper()) {
Log.wtf(TAG, "Must be on the handler thread!");
}
}
@ -950,8 +964,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
/**
* Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
* mHandlerThread}.
* Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
* {@code mHandler}.
*/
private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();

View file

@ -39,6 +39,8 @@ android_test {
"guava-android-testlib",
"mockito-target-extended-minus-junit4",
"net-tests-utils",
"ot-daemon-aidl-java",
"ot-daemon-testing",
"service-connectivity-pre-jarjar",
"service-thread-pre-jarjar",
"truth",
@ -49,12 +51,15 @@ android_test {
"ServiceConnectivityResources",
"framework-wifi",
],
jarjar_rules: ":connectivity-jarjar-rules",
jni_libs: [
"libservice-thread-jni",
// these are needed for Extended Mockito
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
],
jni_uses_platform_apis: true,
jarjar_rules: ":connectivity-jarjar-rules",
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",

View file

@ -30,5 +30,8 @@
<option name="hidden-api-checks" value="false"/>
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
<!-- Ignores tests introduced by frameworks-base-testutils -->
<option name="exclude-filter" value="android.os.test.TestLooperTest"/>
<option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/>
</test>
</configuration>

View file

@ -0,0 +1,163 @@
/*
* 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 com.android.server.thread;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkAgent;
import android.net.NetworkProvider;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IOperationReceiver;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.test.TestLooper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.server.thread.openthread.testing.FakeOtDaemon;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Unit tests for {@link ThreadNetworkControllerService}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class ThreadNetworkControllerServiceTest {
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
// Active Timestamp: 1
// Channel: 19
// Channel Mask: 0x07FFF800
// Ext PAN ID: ACC214689BC40BDF
// Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
// Network Key: F26B3153760F519A63BAFDDFFC80D2AF
// Network Name: OpenThread-d9a0
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private NetworkAgent mMockNetworkAgent;
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
private ThreadNetworkControllerService mService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = ApplicationProvider.getApplicationContext();
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
mFakeOtDaemon = new FakeOtDaemon(handler);
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
mService =
new ThreadNetworkControllerService(
ApplicationProvider.getApplicationContext(),
handler,
networkProvider,
() -> mFakeOtDaemon,
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@Test
public void initialize_tunInterfaceSetToOtDaemon() throws Exception {
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
mService.initialize();
mTestLooper.dispatchAll();
verify(mMockTunIfController, times(1)).createTunInterface();
assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
}
@Test
public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
mTestLooper.dispatchAll();
verify(mockReceiver, never()).onSuccess();
verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
}
@Test
public void join_succeed_threadNetworkRegistered() throws Exception {
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
// Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
// operates on only currently enqueued messages but the delayed message is posted from
// another Handler task.
mTestLooper.dispatchAll();
mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
mTestLooper.dispatchAll();
verify(mockReceiver, times(1)).onSuccess();
verify(mMockNetworkAgent, times(1)).register();
}
}