[Thread] add command to force stop ot-daemon

The Thread HAL supports only one client for a single Thread chip, so
ot-daemon needs to be force stopped in VTS tests because the testapp
needs to connect to the HAL service to perform tests.

Bug: 328538612
Change-Id: I5288e5747ab3957050d0e85c1ea684c6ead6c87d
This commit is contained in:
Kangping Dong 2024-03-19 19:04:11 +08:00
commit 82599d3e21
8 changed files with 356 additions and 52 deletions

View file

@ -16,10 +16,12 @@
package com.android.server.thread;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.ThreadNetworkException;
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
@ -73,6 +75,17 @@ final class ActiveOperationalDatasetReceiverWrapper {
}
}
public void onError(Throwable e) {
if (e instanceof ThreadNetworkException) {
ThreadNetworkException threadException = (ThreadNetworkException) e;
onError(threadException.getErrorCode(), threadException.getMessage());
} else if (e instanceof RemoteException) {
onError(ERROR_INTERNAL_ERROR, "Thread stack error");
} else {
throw new AssertionError(e);
}
}
public void onError(int errorCode, String errorMessage) {
synchronized (sPendingReceiversLock) {
sPendingReceivers.remove(this);

View file

@ -16,9 +16,11 @@
package com.android.server.thread;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import android.net.thread.IOperationReceiver;
import android.net.thread.ThreadNetworkException;
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
@ -29,6 +31,7 @@ import java.util.Set;
/** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
final class OperationReceiverWrapper {
private final IOperationReceiver mReceiver;
private final boolean mExpectOtDaemonDied;
private static final Object sPendingReceiversLock = new Object();
@ -36,7 +39,19 @@ final class OperationReceiverWrapper {
private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
public OperationReceiverWrapper(IOperationReceiver receiver) {
this.mReceiver = receiver;
this(receiver, false /* expectOtDaemonDied */);
}
/**
* Creates a new {@link OperationReceiverWrapper}.
*
* <p>If {@code expectOtDaemonDied} is {@code true}, it's expected that ot-daemon becomes dead
* before {@code receiver} is completed with {@code onSuccess} and {@code onError} and {@code
* receiver#onSuccess} will be invoked in this case.
*/
public OperationReceiverWrapper(IOperationReceiver receiver, boolean expectOtDaemonDied) {
mReceiver = receiver;
mExpectOtDaemonDied = expectOtDaemonDied;
synchronized (sPendingReceiversLock) {
sPendingReceivers.add(this);
@ -47,7 +62,11 @@ final class OperationReceiverWrapper {
synchronized (sPendingReceiversLock) {
for (OperationReceiverWrapper receiver : sPendingReceivers) {
try {
receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
if (receiver.mExpectOtDaemonDied) {
receiver.mReceiver.onSuccess();
} else {
receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
}
} catch (RemoteException e) {
// The client is dead, do nothing
}
@ -68,6 +87,17 @@ final class OperationReceiverWrapper {
}
}
public void onError(Throwable e) {
if (e instanceof ThreadNetworkException) {
ThreadNetworkException threadException = (ThreadNetworkException) e;
onError(threadException.getErrorCode(), threadException.getMessage());
} else if (e instanceof RemoteException) {
onError(ERROR_INTERNAL_ERROR, "Thread stack error");
} else {
throw new AssertionError(e);
}
}
public void onError(int errorCode, String errorMessage, Object... messageArgs) {
synchronized (sPendingReceiversLock) {
sPendingReceivers.remove(this);

View file

@ -95,6 +95,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;
import android.net.thread.ThreadNetworkException.ErrorCode;
import android.os.Build;
import android.os.Handler;
@ -193,6 +194,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@ -311,13 +313,21 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon();
} catch (RemoteException e) {
Log.e(TAG, "Failed to initialize ot-daemon");
Log.e(TAG, "Failed to initialize ot-daemon", e);
} catch (ThreadNetworkException e) {
// no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
throw new AssertionError(e);
}
}
private IOtDaemon getOtDaemon() throws RemoteException {
private IOtDaemon getOtDaemon() throws RemoteException, ThreadNetworkException {
checkOnHandlerThread();
if (mForceStopOtDaemonEnabled) {
throw new ThreadNetworkException(
ERROR_THREAD_DISABLED, "ot-daemon is forcibly stopped");
}
if (mOtDaemon != null) {
return mOtDaemon;
}
@ -406,6 +416,55 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
});
}
/**
* Force stops ot-daemon immediately and prevents ot-daemon from being restarted by
* system_server again.
*
* <p>This is for VTS testing only.
*/
@RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
void forceStopOtDaemonForTest(boolean enabled, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(
() ->
forceStopOtDaemonForTestInternal(
enabled,
new OperationReceiverWrapper(
receiver, true /* expectOtDaemonDied */)));
}
private void forceStopOtDaemonForTestInternal(
boolean enabled, @NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
if (enabled == mForceStopOtDaemonEnabled) {
receiver.onSuccess();
return;
}
if (!enabled) {
mForceStopOtDaemonEnabled = false;
maybeInitializeOtDaemon();
receiver.onSuccess();
return;
}
try {
getOtDaemon().terminate();
// Do not invoke the {@code receiver} callback here but wait for ot-daemon to
// become dead, so that it's guaranteed that ot-daemon is stopped when {@code
// receiver} is completed
} catch (RemoteException e) {
Log.e(TAG, "otDaemon.terminate failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
} catch (ThreadNetworkException e) {
// No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown
throw new AssertionError(e);
} finally {
mForceStopOtDaemonEnabled = true;
}
}
public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
@ -435,9 +494,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
} catch (RemoteException e) {
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
receiver.onError(e);
}
}
@ -494,7 +553,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
/** Returns {@code true} if Thread is set enabled. */
private boolean isEnabled() {
return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
return !mForceStopOtDaemonEnabled
&& !mUserRestricted
&& mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
}
/** Returns {@code true} if Thread has been restricted for the user. */
@ -682,9 +743,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
} catch (RemoteException e) {
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.getChannelMasks failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
receiver.onError(e);
}
}
@ -894,9 +955,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
// The otDaemon.join() will leave first if this device is currently attached
getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException e) {
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.join failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
receiver.onError(e);
}
}
@ -919,9 +980,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
getOtDaemon()
.scheduleMigration(
pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
} catch (RemoteException e) {
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.scheduleMigration failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
receiver.onError(e);
}
}
@ -937,9 +998,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon().leave(newOtStatusReceiver(receiver));
} catch (RemoteException e) {
// Oneway AIDL API should never throw?
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.leave failed", e);
receiver.onError(e);
}
}
@ -970,9 +1031,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
} catch (RemoteException e) {
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.setCountryCode failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
receiver.onError(e);
}
}
@ -1203,8 +1264,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException e) {
// oneway operation should never fail
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.registerStateCallback failed", e);
}
}
@ -1244,8 +1305,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
} catch (RemoteException e) {
// oneway operation should never fail
} catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.registerStateCallback failed", e);
}
}

View file

@ -46,6 +46,7 @@ import java.util.concurrent.TimeoutException;
*/
public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
// These don't require root access.
private static final List<String> NON_PRIVILEGED_COMMANDS =
@ -108,6 +109,8 @@ public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
return setThreadEnabled(true);
case "disable":
return setThreadEnabled(false);
case "force-stop-ot-daemon":
return forceStopOtDaemon();
case "force-country-code":
boolean enabled;
try {
@ -143,34 +146,58 @@ public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
}
private int setThreadEnabled(boolean enabled) {
final PrintWriter perr = getErrorWriter();
CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
mControllerService.setEnabled(
enabled,
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
setEnabledFuture.complete(null);
}
@Override
public void onError(int errorCode, String errorMessage) {
setEnabledFuture.completeExceptionally(
new ThreadNetworkException(errorCode, errorMessage));
}
});
mControllerService.setEnabled(enabled, newOperationReceiver(setEnabledFuture));
return waitForFuture(setEnabledFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
}
private int forceStopOtDaemon() {
final PrintWriter errorWriter = getErrorWriter();
boolean enabled;
try {
setEnabledFuture.get(SET_ENABLED_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
} catch (IllegalArgumentException e) {
errorWriter.println("Invalid argument: " + e.getMessage());
return -1;
}
CompletableFuture<Void> forceStopFuture = new CompletableFuture<>();
mControllerService.forceStopOtDaemonForTest(enabled, newOperationReceiver(forceStopFuture));
return waitForFuture(forceStopFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
}
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
future.complete(null);
}
@Override
public void onError(int errorCode, String errorMessage) {
future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
}
};
}
/**
* Waits for the future to complete within given timeout.
*
* <p>Returns 0 if {@code future} completed successfully, or -1 if {@code future} failed to
* complete. When failed, error messages are printed to {@code errorWriter}.
*/
private int waitForFuture(
CompletableFuture<Void> future, Duration timeout, PrintWriter errorWriter) {
try {
future.get(timeout.toSeconds(), TimeUnit.SECONDS);
return 0;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
perr.println("Failed: " + e.getMessage());
errorWriter.println("Failed: " + e.getMessage());
} catch (ExecutionException e) {
perr.println("Failed: " + e.getCause().getMessage());
errorWriter.println("Failed: " + e.getCause().getMessage());
} catch (TimeoutException e) {
perr.println("Failed: command timeout for " + SET_ENABLED_TIMEOUT);
errorWriter.println("Failed: command timeout for " + timeout);
}
return -1;
@ -210,6 +237,8 @@ public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
private void onHelpPrivileged(PrintWriter pw) {
pw.println(" force-country-code enabled <two-letter code> | disabled ");
pw.println(" Sets country code to <two-letter code> or left for normal value");
pw.println(" force-stop-ot-daemon enabled | disabled ");
pw.println(" force stop ot-daemon service");
}
@Override

View file

@ -18,11 +18,14 @@ package android.net.thread;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
@ -32,10 +35,14 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.ExecutionException;
/** Integration tests for {@link ThreadNetworkShellCommand}. */
@LargeTest
@RequiresThreadFeature
@ -47,6 +54,21 @@ public class ThreadNetworkShellCommandTest {
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
@Before
public void setUp() {
ensureThreadEnabled();
}
@After
public void tearDown() {
ensureThreadEnabled();
}
private static void ensureThreadEnabled() {
runThreadCommand("force-stop-ot-daemon disabled");
runThreadCommand("enable");
}
@Test
public void enable_threadStateIsEnabled() throws Exception {
runThreadCommand("enable");
@ -61,6 +83,38 @@ public class ThreadNetworkShellCommandTest {
assertThat(mController.getEnabledState()).isEqualTo(STATE_DISABLED);
}
@Test
public void forceStopOtDaemon_forceStopEnabled_otDaemonServiceDisappear() {
runThreadCommand("force-stop-ot-daemon enabled");
assertThat(runShellCommandOrThrow("service list")).doesNotContain("ot_daemon");
}
@Test
public void forceStopOtDaemon_forceStopEnabled_canNotEnableThread() throws Exception {
runThreadCommand("force-stop-ot-daemon enabled");
ExecutionException thrown =
assertThrows(ExecutionException.class, () -> mController.setEnabledAndWait(true));
ThreadNetworkException cause = (ThreadNetworkException) thrown.getCause();
assertThat(cause.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
}
@Test
public void forceStopOtDaemon_forceStopDisabled_otDaemonServiceAppears() throws Exception {
runThreadCommand("force-stop-ot-daemon disabled");
assertThat(runShellCommandOrThrow("service list")).contains("ot_daemon");
}
@Test
public void forceStopOtDaemon_forceStopDisabled_canEnableThread() throws Exception {
runThreadCommand("force-stop-ot-daemon disabled");
mController.setEnabledAndWait(true);
assertThat(mController.getEnabledState()).isEqualTo(STATE_ENABLED);
}
@Test
public void forceCountryCode_setCN_getCountryCodeReturnsCN() {
runThreadCommand("force-country-code enabled CN");
@ -70,6 +124,6 @@ public class ThreadNetworkShellCommandTest {
}
private static String runThreadCommand(String cmd) {
return runShellCommand("cmd thread_network " + cmd);
return runShellCommandOrThrow("cmd thread_network " + cmd);
}
}

View file

@ -46,6 +46,7 @@ public final class ThreadNetworkControllerWrapper {
public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(10);
public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
private final ThreadNetworkController mController;
@ -115,6 +116,18 @@ public final class ThreadNetworkControllerWrapper {
}
}
/** An synchronous variant of {@link ThreadNetworkController#setEnabled}. */
public void setEnabledAndWait(boolean enabled)
throws InterruptedException, ExecutionException, TimeoutException {
CompletableFuture<Void> future = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() ->
mController.setEnabled(
enabled, directExecutor(), newOutcomeReceiver(future)));
future.get(SET_ENABLED_TIMEOUT.toSeconds(), SECONDS);
}
/** Joins the given network and wait for this device to become attached. */
public void joinAndWait(ActiveOperationalDataset activeDataset)
throws InterruptedException, ExecutionException, TimeoutException {

View file

@ -21,6 +21,7 @@ import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
@ -37,6 +38,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@ -440,4 +442,51 @@ public final class ThreadNetworkControllerServiceTest {
verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class));
verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
}
@Test
public void forceStopOtDaemonForTest_noPermission_throwsSecurityException() {
doThrow(new SecurityException(""))
.when(mContext)
.enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), any());
assertThrows(
SecurityException.class,
() -> mService.forceStopOtDaemonForTest(true, new IOperationReceiver.Default()));
}
@Test
public void forceStopOtDaemonForTest_enabled_otDaemonDiesAndJoinFails() throws Exception {
mService.initialize();
IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class);
mService.forceStopOtDaemonForTest(true, mockReceiver);
mTestLooper.dispatchAll();
mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver);
mTestLooper.dispatchAll();
verify(mockReceiver, times(1)).onSuccess();
assertThat(mFakeOtDaemon.isInitialized()).isFalse();
verify(mockJoinReceiver, times(1)).onError(eq(ERROR_THREAD_DISABLED), anyString());
}
@Test
public void forceStopOtDaemonForTest_disable_otDaemonRestartsAndJoinSccess() throws Exception {
mService.initialize();
IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class);
mService.forceStopOtDaemonForTest(true, mock(IOperationReceiver.class));
mTestLooper.dispatchAll();
mService.forceStopOtDaemonForTest(false, mockReceiver);
mTestLooper.dispatchAll();
mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver);
mTestLooper.dispatchAll();
mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
mTestLooper.dispatchAll();
verify(mockReceiver, times(1)).onSuccess();
assertThat(mFakeOtDaemon.isInitialized()).isTrue();
verify(mockJoinReceiver, times(1)).onSuccess();
}
}

View file

@ -16,10 +16,15 @@
package com.android.server.thread;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.contains;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -50,15 +55,14 @@ public class ThreadNetworkShellCommandTest {
@Mock PrintWriter mErrorWriter;
@Mock PrintWriter mOutputWriter;
ThreadNetworkShellCommand mThreadNetworkShellCommand;
ThreadNetworkShellCommand mShellCommand;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mThreadNetworkShellCommand =
new ThreadNetworkShellCommand(mControllerService, mCountryCode);
mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
mShellCommand = new ThreadNetworkShellCommand(mControllerService, mCountryCode);
mShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
}
@After
@ -71,7 +75,7 @@ public class ThreadNetworkShellCommandTest {
BinderUtil.setUid(Process.SHELL_UID);
when(mCountryCode.getCountryCode()).thenReturn("US");
mThreadNetworkShellCommand.exec(
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@ -85,7 +89,7 @@ public class ThreadNetworkShellCommandTest {
public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
BinderUtil.setUid(Process.SHELL_UID);
mThreadNetworkShellCommand.exec(
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@ -100,7 +104,7 @@ public class ThreadNetworkShellCommandTest {
public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
BinderUtil.setUid(Process.ROOT_UID);
mThreadNetworkShellCommand.exec(
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@ -114,7 +118,7 @@ public class ThreadNetworkShellCommandTest {
public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
BinderUtil.setUid(Process.SHELL_UID);
mThreadNetworkShellCommand.exec(
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@ -129,7 +133,7 @@ public class ThreadNetworkShellCommandTest {
public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
BinderUtil.setUid(Process.ROOT_UID);
mThreadNetworkShellCommand.exec(
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@ -138,4 +142,55 @@ public class ThreadNetworkShellCommandTest {
verify(mCountryCode).clearOverrideCountryCode();
}
@Test
public void forceStopOtDaemon_executeInUnrootedShell_failedAndServiceApiNotCalled() {
BinderUtil.setUid(Process.SHELL_UID);
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-stop-ot-daemon", "enabled"});
verify(mControllerService, never()).forceStopOtDaemonForTest(anyBoolean(), any());
verify(mErrorWriter, atLeastOnce()).println(contains("force-stop-ot-daemon"));
verify(mOutputWriter, never()).println();
}
@Test
public void forceStopOtDaemon_serviceThrows_failed() {
BinderUtil.setUid(Process.ROOT_UID);
doThrow(new SecurityException(""))
.when(mControllerService)
.forceStopOtDaemonForTest(eq(true), any());
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-stop-ot-daemon", "enabled"});
verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
verify(mOutputWriter, never()).println();
}
@Test
public void forceStopOtDaemon_serviceApiTimeout_failedWithTimeoutError() {
BinderUtil.setUid(Process.ROOT_UID);
doNothing().when(mControllerService).forceStopOtDaemonForTest(eq(true), any());
mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-stop-ot-daemon", "enabled"});
verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
verify(mOutputWriter, never()).println();
}
}