Merge changes I76b0a7ba,If7308f1a,Ica8faced,Ia8f42ed0,I975afb45, ... into main
* changes: Add some documentation to EthernetInterfaceStateMachine Add support for link state tracking Add unit test class for EthernetInterfaceStateMachine Add missing call to add states to state machine Make class testable Log.wtf if block times out Wait on IpClient#onQuit in StartedState#exit Use TAG instead Introduce CMD_ON_NETWORK_NEEDED and CMD_ON_NETWORK_UNNEEDED messages Clean up and document state lifecycle Add convenience method for processMessage with no arguments Implement NetworkOffer in EthernetInterfaceStateMachine Add boilerplate to represent interface states in state machine Fix typo in SyncStateMachineTest Move SyncStateMachine to staticlibs
This commit is contained in:
commit
58526e4d96
8 changed files with 458 additions and 9 deletions
|
|
@ -75,6 +75,7 @@ import com.android.net.module.util.InterfaceParams;
|
|||
import com.android.net.module.util.NetdUtils;
|
||||
import com.android.net.module.util.SdkUtil.LateSdk;
|
||||
import com.android.net.module.util.SharedLog;
|
||||
import com.android.net.module.util.SyncStateMachine.StateInfo;
|
||||
import com.android.net.module.util.ip.InterfaceController;
|
||||
import com.android.net.module.util.ip.IpNeighborMonitor;
|
||||
import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
|
||||
|
|
@ -87,7 +88,6 @@ import com.android.networkstack.tethering.metrics.TetheringMetrics;
|
|||
import com.android.networkstack.tethering.util.InterfaceSet;
|
||||
import com.android.networkstack.tethering.util.PrefixUtils;
|
||||
import com.android.networkstack.tethering.util.StateMachineShim;
|
||||
import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ import android.os.Looper;
|
|||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.State;
|
||||
import com.android.internal.util.StateMachine;
|
||||
import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
|
||||
import com.android.net.module.util.SyncStateMachine;
|
||||
import com.android.net.module.util.SyncStateMachine.StateInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ import android.os.Looper
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.util.State
|
||||
import com.android.net.module.util.SyncStateMachine
|
||||
import com.android.net.module.util.SyncStateMachine.StateInfo
|
||||
import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
|
||||
import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
|
||||
import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
|
||||
import kotlin.test.assertFailsWith
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* 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 com.android.server.ethernet;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkProvider;
|
||||
import android.net.NetworkProvider.NetworkOfferCallback;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.NetworkScore;
|
||||
import android.net.ip.IIpClient;
|
||||
import android.net.ip.IpClientCallbacks;
|
||||
import android.net.ip.IpClientManager;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.util.State;
|
||||
import com.android.net.module.util.SyncStateMachine;
|
||||
import com.android.net.module.util.SyncStateMachine.StateInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* EthernetInterfaceStateMachine manages the lifecycle of an ethernet-like network interface which
|
||||
* includes managing a NetworkOffer, IpClient, and NetworkAgent as well as making the interface
|
||||
* available as a tethering downstream.
|
||||
*
|
||||
* All methods exposed by this class *must* be called on the Handler thread provided in the
|
||||
* constructor.
|
||||
*/
|
||||
class EthernetInterfaceStateMachine extends SyncStateMachine {
|
||||
private static final String TAG = EthernetInterfaceStateMachine.class.getSimpleName();
|
||||
|
||||
private static final int CMD_ON_LINK_UP = 1;
|
||||
private static final int CMD_ON_LINK_DOWN = 2;
|
||||
private static final int CMD_ON_NETWORK_NEEDED = 3;
|
||||
private static final int CMD_ON_NETWORK_UNNEEDED = 4;
|
||||
private static final int CMD_ON_IPCLIENT_CREATED = 5;
|
||||
|
||||
private class EthernetNetworkOfferCallback implements NetworkOfferCallback {
|
||||
private final Set<Integer> mRequestIds = new ArraySet<>();
|
||||
|
||||
@Override
|
||||
public void onNetworkNeeded(@NonNull NetworkRequest request) {
|
||||
if (this != mNetworkOfferCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
mRequestIds.add(request.requestId);
|
||||
if (mRequestIds.size() == 1) {
|
||||
processMessage(CMD_ON_NETWORK_NEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkUnneeded(@NonNull NetworkRequest request) {
|
||||
if (this != mNetworkOfferCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mRequestIds.remove(request.requestId)) {
|
||||
// This can only happen if onNetworkNeeded was not called for a request or if
|
||||
// the requestId changed. Both should *never* happen.
|
||||
Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
|
||||
}
|
||||
if (mRequestIds.isEmpty()) {
|
||||
processMessage(CMD_ON_NETWORK_UNNEEDED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EthernetIpClientCallback extends IpClientCallbacks {
|
||||
private final ConditionVariable mOnQuitCv = new ConditionVariable(false);
|
||||
|
||||
private void safelyPostOnHandler(Runnable r) {
|
||||
mHandler.post(() -> {
|
||||
if (this != mIpClientCallback) {
|
||||
return;
|
||||
}
|
||||
r.run();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIpClientCreated(IIpClient ipClient) {
|
||||
safelyPostOnHandler(() -> {
|
||||
// TODO: add a SyncStateMachine#processMessage(cmd, obj) overload.
|
||||
processMessage(CMD_ON_IPCLIENT_CREATED, 0, 0,
|
||||
mDependencies.makeIpClientManager(ipClient));
|
||||
});
|
||||
}
|
||||
|
||||
public void waitOnQuit() {
|
||||
if (!mOnQuitCv.block(5_000 /* timeoutMs */)) {
|
||||
Log.wtf(TAG, "Timed out waiting on IpClient to shutdown.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuit() {
|
||||
mOnQuitCv.open();
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable EthernetNetworkOfferCallback mNetworkOfferCallback;
|
||||
private @Nullable EthernetIpClientCallback mIpClientCallback;
|
||||
private @Nullable IpClientManager mIpClient;
|
||||
private final String mIface;
|
||||
private final Handler mHandler;
|
||||
private final Context mContext;
|
||||
private final NetworkCapabilities mCapabilities;
|
||||
private final NetworkProvider mNetworkProvider;
|
||||
private final EthernetNetworkFactory.Dependencies mDependencies;
|
||||
private boolean mLinkUp = false;
|
||||
|
||||
/** Interface is in tethering mode. */
|
||||
private class TetheringState extends State {
|
||||
@Override
|
||||
public boolean processMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case CMD_ON_LINK_UP:
|
||||
case CMD_ON_LINK_DOWN:
|
||||
// TODO: think about what to do here.
|
||||
return HANDLED;
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
}
|
||||
}
|
||||
|
||||
/** Link is down */
|
||||
private class LinkDownState extends State {
|
||||
@Override
|
||||
public boolean processMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case CMD_ON_LINK_UP:
|
||||
transitionTo(mStoppedState);
|
||||
return HANDLED;
|
||||
case CMD_ON_LINK_DOWN:
|
||||
// do nothing, already in the correct state.
|
||||
return HANDLED;
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
}
|
||||
}
|
||||
|
||||
/** Parent states of all states that do not cause a NetworkOffer to be extended. */
|
||||
private class NetworkOfferExtendedState extends State {
|
||||
@Override
|
||||
public void enter() {
|
||||
if (mNetworkOfferCallback != null) {
|
||||
// This should never happen. If it happens anyway, log and move on.
|
||||
Log.wtf(TAG, "Previous NetworkOffer was never retracted");
|
||||
}
|
||||
|
||||
mNetworkOfferCallback = new EthernetNetworkOfferCallback();
|
||||
final NetworkScore defaultScore = new NetworkScore.Builder().build();
|
||||
mNetworkProvider.registerNetworkOffer(defaultScore,
|
||||
new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
|
||||
mNetworkOfferCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case CMD_ON_LINK_UP:
|
||||
// do nothing, already in the correct state.
|
||||
return HANDLED;
|
||||
case CMD_ON_LINK_DOWN:
|
||||
transitionTo(mLinkDownState);
|
||||
return HANDLED;
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
|
||||
mNetworkOfferCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offer is extended but has not been requested.
|
||||
*
|
||||
* StoppedState's sole purpose is to react to a CMD_ON_NETWORK_NEEDED and transition to
|
||||
* StartedState when that happens. Note that StoppedState could be rolled into
|
||||
* NetworkOfferExtendedState. However, keeping the states separate provides some additional
|
||||
* protection by logging a Log.wtf if a CMD_ON_NETWORK_NEEDED is received in an unexpected state
|
||||
* (i.e. StartedState or RunningState). StoppedState is a child of NetworkOfferExtendedState.
|
||||
*/
|
||||
private class StoppedState extends State {
|
||||
@Override
|
||||
public boolean processMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case CMD_ON_NETWORK_NEEDED:
|
||||
transitionTo(mStartedState);
|
||||
return HANDLED;
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
}
|
||||
}
|
||||
|
||||
/** Network is needed, starts IpClient and manages its lifecycle */
|
||||
private class StartedState extends State {
|
||||
@Override
|
||||
public void enter() {
|
||||
mIpClientCallback = new EthernetIpClientCallback();
|
||||
mDependencies.makeIpClient(mContext, mIface, mIpClientCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case CMD_ON_NETWORK_UNNEEDED:
|
||||
transitionTo(mStoppedState);
|
||||
return HANDLED;
|
||||
case CMD_ON_IPCLIENT_CREATED:
|
||||
mIpClient = (IpClientManager) msg.obj;
|
||||
transitionTo(mRunningState);
|
||||
return HANDLED;
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
if (mIpClient != null) {
|
||||
mIpClient.shutdown();
|
||||
// TODO: consider adding a StoppingState and making the shutdown operation
|
||||
// asynchronous.
|
||||
mIpClientCallback.waitOnQuit();
|
||||
}
|
||||
mIpClientCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** IpClient is running, starts provisioning and registers NetworkAgent */
|
||||
private class RunningState extends State {
|
||||
|
||||
}
|
||||
|
||||
private final TetheringState mTetheringState = new TetheringState();
|
||||
private final LinkDownState mLinkDownState = new LinkDownState();
|
||||
private final NetworkOfferExtendedState mOfferExtendedState = new NetworkOfferExtendedState();
|
||||
private final StoppedState mStoppedState = new StoppedState();
|
||||
private final StartedState mStartedState = new StartedState();
|
||||
private final RunningState mRunningState = new RunningState();
|
||||
|
||||
public EthernetInterfaceStateMachine(String iface, Handler handler, Context context,
|
||||
NetworkCapabilities capabilities, NetworkProvider provider,
|
||||
EthernetNetworkFactory.Dependencies deps) {
|
||||
super(TAG + "." + iface, handler.getLooper().getThread());
|
||||
|
||||
mIface = iface;
|
||||
mHandler = handler;
|
||||
mContext = context;
|
||||
mCapabilities = capabilities;
|
||||
mNetworkProvider = provider;
|
||||
mDependencies = deps;
|
||||
|
||||
// Interface lifecycle:
|
||||
// [ LinkDownState ]
|
||||
// |
|
||||
// v
|
||||
// *link comes up*
|
||||
// |
|
||||
// v
|
||||
// [ StoppedState ]
|
||||
// |
|
||||
// v
|
||||
// *network is needed*
|
||||
// |
|
||||
// v
|
||||
// [ StartedState ]
|
||||
// |
|
||||
// v
|
||||
// *IpClient is created*
|
||||
// |
|
||||
// v
|
||||
// [ RunningState ]
|
||||
// |
|
||||
// v
|
||||
// *interface is requested for tethering*
|
||||
// |
|
||||
// v
|
||||
// [TetheringState]
|
||||
//
|
||||
// Tethering mode is special as the interface is configured by Tethering, rather than the
|
||||
// ethernet module.
|
||||
final List<StateInfo> states = new ArrayList<>();
|
||||
states.add(new StateInfo(mTetheringState, null));
|
||||
|
||||
// CHECKSTYLE:OFF IndentationCheck
|
||||
// Initial state
|
||||
states.add(new StateInfo(mLinkDownState, null));
|
||||
states.add(new StateInfo(mOfferExtendedState, null));
|
||||
states.add(new StateInfo(mStoppedState, mOfferExtendedState));
|
||||
states.add(new StateInfo(mStartedState, mOfferExtendedState));
|
||||
states.add(new StateInfo(mRunningState, mStartedState));
|
||||
// CHECKSTYLE:ON IndentationCheck
|
||||
addAllStates(states);
|
||||
|
||||
// TODO: set initial state to TetheringState if a tethering interface has been requested and
|
||||
// this is the first interface to be added.
|
||||
start(mLinkDownState);
|
||||
}
|
||||
|
||||
public boolean updateLinkState(boolean up) {
|
||||
if (mLinkUp == up) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: consider setting mLinkUp as part of processMessage().
|
||||
mLinkUp = up;
|
||||
if (!up) { // was up, goes down
|
||||
processMessage(CMD_ON_LINK_DOWN);
|
||||
} else { // was down, comes up
|
||||
processMessage(CMD_ON_LINK_UP);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,12 +39,13 @@ java_library {
|
|||
"device/com/android/net/module/util/DeviceConfigUtils.java",
|
||||
"device/com/android/net/module/util/DomainUtils.java",
|
||||
"device/com/android/net/module/util/FdEventsReader.java",
|
||||
"device/com/android/net/module/util/FeatureVersions.java",
|
||||
"device/com/android/net/module/util/HandlerUtils.java",
|
||||
"device/com/android/net/module/util/NetworkMonitorUtils.java",
|
||||
"device/com/android/net/module/util/PacketReader.java",
|
||||
"device/com/android/net/module/util/SharedLog.java",
|
||||
"device/com/android/net/module/util/SocketUtils.java",
|
||||
"device/com/android/net/module/util/FeatureVersions.java",
|
||||
"device/com/android/net/module/util/HandlerUtils.java",
|
||||
"device/com/android/net/module/util/SyncStateMachine.java",
|
||||
// This library is used by system modules, for which the system health impact of Kotlin
|
||||
// has not yet been evaluated. Annotations may need jarjar'ing.
|
||||
// "src_devicecommon/**/*.kt",
|
||||
|
|
@ -68,6 +69,7 @@ java_library {
|
|||
"//packages/modules/CaptivePortalLogin",
|
||||
],
|
||||
static_libs: [
|
||||
"modules-utils-statemachine",
|
||||
"net-utils-framework-common",
|
||||
],
|
||||
libs: [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,347 @@
|
|||
/**
|
||||
* 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.net.module.util;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Message;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.util.State;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An implementation of a state machine, meant to be called synchronously.
|
||||
*
|
||||
* This class implements a finite state automaton based on the same State
|
||||
* class as StateMachine.
|
||||
* All methods of this class must be called on only one thread.
|
||||
*/
|
||||
public class SyncStateMachine {
|
||||
@NonNull private final String mName;
|
||||
@NonNull private final Thread mMyThread;
|
||||
private final boolean mDbg;
|
||||
private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
|
||||
|
||||
// mCurrentState is the current state. mDestState is the target state that mCurrentState will
|
||||
// transition to. The value of mDestState can be changed when a state processes a message and
|
||||
// calls #transitionTo, but it cannot be changed during the state transition. When the state
|
||||
// transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
|
||||
// mDestState only be null before state machine starts and must only be touched on mMyThread.
|
||||
@Nullable private State mCurrentState;
|
||||
@Nullable private State mDestState;
|
||||
private final ArrayDeque<Message> mSelfMsgQueue = new ArrayDeque<Message>();
|
||||
|
||||
// MIN_VALUE means not currently processing any message.
|
||||
private int mCurrentlyProcessing = Integer.MIN_VALUE;
|
||||
// Indicates whether automaton can send self message. Self messages can only be sent by
|
||||
// automaton from State#enter, State#exit, or State#processMessage. Calling from outside
|
||||
// of State is not allowed.
|
||||
private boolean mSelfMsgAllowed = false;
|
||||
|
||||
/**
|
||||
* A information class about a state and its parent. Used to maintain the state hierarchy.
|
||||
*/
|
||||
public static class StateInfo {
|
||||
/** The state who owns this StateInfo. */
|
||||
public final State state;
|
||||
/** The parent state. */
|
||||
public final State parent;
|
||||
// True when the state has been entered and on the stack.
|
||||
private boolean mActive;
|
||||
|
||||
public StateInfo(@NonNull final State child, @Nullable final State parent) {
|
||||
this.state = child;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param name of this machine.
|
||||
* @param thread the running thread of this machine. It must either be the thread on which this
|
||||
* constructor is called, or a thread that is not started yet.
|
||||
*/
|
||||
public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
|
||||
this(name, thread, false /* debug */);
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param name of this machine.
|
||||
* @param thread the running thread of this machine. It must either be the thread on which this
|
||||
* constructor is called, or a thread that is not started yet.
|
||||
* @param dbg whether to print debug logs.
|
||||
*/
|
||||
public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
|
||||
final boolean dbg) {
|
||||
mMyThread = thread;
|
||||
// Machine can either be setup from machine thread or before machine thread started.
|
||||
ensureCorrectOrNotStartedThread();
|
||||
|
||||
mName = name;
|
||||
mDbg = dbg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all of states to the state machine. Different StateInfos which have same state are not
|
||||
* allowed. In other words, a state can not have multiple parent states. #addAllStates can
|
||||
* only be called once either from mMyThread or before mMyThread started.
|
||||
*/
|
||||
public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
|
||||
ensureCorrectOrNotStartedThread();
|
||||
|
||||
if (mCurrentState != null) {
|
||||
throw new IllegalStateException("State only can be added before started");
|
||||
}
|
||||
|
||||
if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
|
||||
|
||||
if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
|
||||
|
||||
final Set<Class> usedClasses = new ArraySet<>();
|
||||
for (final StateInfo info : stateInfos) {
|
||||
Objects.requireNonNull(info.state);
|
||||
if (!usedClasses.add(info.state.getClass())) {
|
||||
throw new IllegalStateException("Adding the same state multiple times in a state "
|
||||
+ "machine is forbidden because it tends to be confusing; it can be done "
|
||||
+ "with anonymous subclasses but consider carefully whether you want to "
|
||||
+ "use a single state or other alternatives instead.");
|
||||
}
|
||||
|
||||
mStateInfo.put(info.state, info);
|
||||
}
|
||||
|
||||
// Check whether all of parent states indicated from StateInfo are added.
|
||||
for (final StateInfo info : stateInfos) {
|
||||
if (info.parent != null) ensureExistingState(info.parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the state machine. The initial state can't be child state.
|
||||
*
|
||||
* @param initialState the first state of this machine. The state must be exact state object
|
||||
* setting up by {@link #addAllStates}, not a copy of it.
|
||||
*/
|
||||
public final void start(@NonNull final State initialState) {
|
||||
ensureCorrectThread();
|
||||
ensureExistingState(initialState);
|
||||
|
||||
mDestState = initialState;
|
||||
mSelfMsgAllowed = true;
|
||||
performTransitions();
|
||||
mSelfMsgAllowed = false;
|
||||
// If sendSelfMessage was called inside initialState#enter(), mSelfMsgQueue must be
|
||||
// processed.
|
||||
maybeProcessSelfMessageQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the message synchronously then perform state transition. This method is used
|
||||
* externally to the automaton to request that the automaton process the given message.
|
||||
* The message is processed sequentially, so calling this method recursively is not permitted.
|
||||
* In other words, using this method inside State#enter, State#exit, or State#processMessage
|
||||
* is incorrect and will result in an IllegalStateException.
|
||||
*
|
||||
* @param what is assigned to Message.what
|
||||
* @param arg1 is assigned to Message.arg1
|
||||
* @param arg2 is assigned to Message.arg2
|
||||
* @param obj is assigned to Message.obj
|
||||
*/
|
||||
public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
|
||||
ensureCorrectThread();
|
||||
|
||||
if (mCurrentlyProcessing != Integer.MIN_VALUE) {
|
||||
throw new IllegalStateException("Message(" + mCurrentlyProcessing
|
||||
+ ") is still being processed");
|
||||
}
|
||||
|
||||
// mCurrentlyProcessing tracks the external message request and it prevents this method to
|
||||
// be called recursively. Once this message is processed and the transitions have been
|
||||
// performed, the automaton will process the self message queue. The messages in the self
|
||||
// message queue are added from within the automaton during processing external message.
|
||||
// mCurrentlyProcessing is still the original external one and it will not prevent self
|
||||
// messages from being processed.
|
||||
mCurrentlyProcessing = what;
|
||||
final Message msg = Message.obtain(null, what, arg1, arg2, obj);
|
||||
currentStateProcessMessageThenPerformTransitions(msg);
|
||||
msg.recycle();
|
||||
maybeProcessSelfMessageQueue();
|
||||
|
||||
mCurrentlyProcessing = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously process a message and perform state transition.
|
||||
*
|
||||
* @param what is assigned to Message.what.
|
||||
*/
|
||||
public final void processMessage(int what) {
|
||||
processMessage(what, 0, 0, null);
|
||||
}
|
||||
|
||||
private void maybeProcessSelfMessageQueue() {
|
||||
while (!mSelfMsgQueue.isEmpty()) {
|
||||
currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
|
||||
}
|
||||
}
|
||||
|
||||
private void currentStateProcessMessageThenPerformTransitions(@NonNull final Message msg) {
|
||||
mSelfMsgAllowed = true;
|
||||
StateInfo consideredState = mStateInfo.get(mCurrentState);
|
||||
while (null != consideredState) {
|
||||
// Ideally this should compare with IState.HANDLED, but it is not public field so just
|
||||
// checking whether the return value is true (IState.HANDLED = true).
|
||||
if (consideredState.state.processMessage(msg)) {
|
||||
if (mDbg) {
|
||||
Log.d(mName, "State " + consideredState.state
|
||||
+ " processed message " + msg.what);
|
||||
}
|
||||
break;
|
||||
}
|
||||
consideredState = mStateInfo.get(consideredState.parent);
|
||||
}
|
||||
if (null == consideredState) {
|
||||
Log.wtf(mName, "Message " + msg.what + " was not handled");
|
||||
}
|
||||
|
||||
performTransitions();
|
||||
mSelfMsgAllowed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send self message during state transition.
|
||||
*
|
||||
* Must only be used inside State processMessage, enter or exit. The typical use case is
|
||||
* something wrong happens during state transition, sending an error message which would be
|
||||
* handled after finishing current state transitions.
|
||||
*/
|
||||
public final void sendSelfMessage(int what, int arg1, int arg2, Object obj) {
|
||||
if (!mSelfMsgAllowed) {
|
||||
throw new IllegalStateException("sendSelfMessage can only be called inside "
|
||||
+ "State#enter, State#exit or State#processMessage");
|
||||
}
|
||||
|
||||
mSelfMsgQueue.add(Message.obtain(null, what, arg1, arg2, obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition to destination state. Upon returning from processMessage the automaton will
|
||||
* transition to the given destination state.
|
||||
*
|
||||
* This function can NOT be called inside the State enter and exit function. The transition
|
||||
* target is always defined and can never be changed mid-way of state transition.
|
||||
*
|
||||
* @param destState will be the state to transition to. The state must be the same instance set
|
||||
* up by {@link #addAllStates}, not a copy of it.
|
||||
*/
|
||||
public final void transitionTo(@NonNull final State destState) {
|
||||
if (mDbg) Log.d(mName, "transitionTo " + destState);
|
||||
ensureCorrectThread();
|
||||
ensureExistingState(destState);
|
||||
|
||||
if (mDestState == mCurrentState) {
|
||||
mDestState = destState;
|
||||
} else {
|
||||
throw new IllegalStateException("Destination already specified");
|
||||
}
|
||||
}
|
||||
|
||||
private void performTransitions() {
|
||||
// 1. Determine the common ancestor state of current/destination states
|
||||
// 2. Invoke state exit list from current state to common ancestor state.
|
||||
// 3. Invoke state enter list from common ancestor state to destState by going
|
||||
// through mEnterStateStack.
|
||||
if (mDestState == mCurrentState) return;
|
||||
|
||||
final StateInfo commonAncestor = getLastActiveAncestor(mStateInfo.get(mDestState));
|
||||
|
||||
executeExitMethods(commonAncestor, mStateInfo.get(mCurrentState));
|
||||
executeEnterMethods(commonAncestor, mStateInfo.get(mDestState));
|
||||
mCurrentState = mDestState;
|
||||
}
|
||||
|
||||
// Null is the root of all states.
|
||||
private StateInfo getLastActiveAncestor(@Nullable final StateInfo start) {
|
||||
if (null == start || start.mActive) return start;
|
||||
|
||||
return getLastActiveAncestor(mStateInfo.get(start.parent));
|
||||
}
|
||||
|
||||
// Call the exit method from current state to common ancestor state.
|
||||
// Both the commonAncestor and exitingState StateInfo can be null because null is the ancestor
|
||||
// of all states.
|
||||
// For example: When transitioning from state1 to state2, the
|
||||
// executeExitMethods(commonAncestor, exitingState) function will be called twice, once with
|
||||
// null and state1 as the argument, and once with null and null as the argument.
|
||||
// root
|
||||
// | \
|
||||
// current <- state1 state2 -> destination
|
||||
private void executeExitMethods(@Nullable StateInfo commonAncestor,
|
||||
@Nullable StateInfo exitingState) {
|
||||
if (commonAncestor == exitingState) return;
|
||||
|
||||
if (mDbg) Log.d(mName, exitingState.state + " exit()");
|
||||
exitingState.state.exit();
|
||||
exitingState.mActive = false;
|
||||
executeExitMethods(commonAncestor, mStateInfo.get(exitingState.parent));
|
||||
}
|
||||
|
||||
// Call the enter method from common ancestor state to destination state.
|
||||
// Both the commonAncestor and enteringState StateInfo can be null because null is the ancestor
|
||||
// of all states.
|
||||
// For example: When transitioning from state1 to state2, the
|
||||
// executeEnterMethods(commonAncestor, enteringState) function will be called twice, once with
|
||||
// null and state2 as the argument, and once with null and null as the argument.
|
||||
// root
|
||||
// | \
|
||||
// current <- state1 state2 -> destination
|
||||
private void executeEnterMethods(@Nullable StateInfo commonAncestor,
|
||||
@Nullable StateInfo enteringState) {
|
||||
if (enteringState == commonAncestor) return;
|
||||
|
||||
executeEnterMethods(commonAncestor, mStateInfo.get(enteringState.parent));
|
||||
if (mDbg) Log.d(mName, enteringState.state + " enter()");
|
||||
enteringState.state.enter();
|
||||
enteringState.mActive = true;
|
||||
}
|
||||
|
||||
private void ensureCorrectThread() {
|
||||
if (!mMyThread.equals(Thread.currentThread())) {
|
||||
throw new IllegalStateException("Called from wrong thread");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureCorrectOrNotStartedThread() {
|
||||
if (!mMyThread.isAlive()) return;
|
||||
|
||||
ensureCorrectThread();
|
||||
}
|
||||
|
||||
private void ensureExistingState(@NonNull final State state) {
|
||||
if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
/**
|
||||
* 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.net.module.util
|
||||
|
||||
import android.os.Message
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.util.State
|
||||
import com.android.net.module.util.SyncStateMachine.StateInfo
|
||||
import java.util.ArrayDeque
|
||||
import java.util.ArrayList
|
||||
import kotlin.test.assertFailsWith
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.spy
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
|
||||
private const val MSG_INVALID = -1
|
||||
private const val MSG_1 = 1
|
||||
private const val MSG_2 = 2
|
||||
private const val MSG_3 = 3
|
||||
private const val MSG_4 = 4
|
||||
private const val MSG_5 = 5
|
||||
private const val MSG_6 = 6
|
||||
private const val MSG_7 = 7
|
||||
private const val ARG_1 = 100
|
||||
private const val ARG_2 = 200
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class SyncStateMachineTest {
|
||||
private val mState1 = spy(object : TestState(MSG_1) {})
|
||||
private val mState2 = spy(object : TestState(MSG_2) {})
|
||||
private val mState3 = spy(object : TestState(MSG_3) {})
|
||||
private val mState4 = spy(object : TestState(MSG_4) {})
|
||||
private val mState5 = spy(object : TestState(MSG_5) {})
|
||||
private val mState6 = spy(object : TestState(MSG_6) {})
|
||||
private val mState7 = spy(object : TestState(MSG_7) {})
|
||||
private val mInOrder = inOrder(mState1, mState2, mState3, mState4, mState5, mState6, mState7)
|
||||
// Lazy initialize to make sure running in test thread.
|
||||
private val mSM by lazy {
|
||||
SyncStateMachine("TestSyncStateMachine", Thread.currentThread(), true /* debug */)
|
||||
}
|
||||
private val mAllStates = ArrayList<StateInfo>()
|
||||
|
||||
private val mMsgProcessedResults = ArrayDeque<Pair<State, Int>>()
|
||||
|
||||
open inner class TestState(val expected: Int) : State() {
|
||||
// Control destination state in obj field for testing.
|
||||
override fun processMessage(msg: Message): Boolean {
|
||||
mMsgProcessedResults.add(this to msg.what)
|
||||
assertEquals(ARG_1, msg.arg1)
|
||||
assertEquals(ARG_2, msg.arg2)
|
||||
|
||||
if (msg.what == expected) {
|
||||
msg.obj?.let { mSM.transitionTo(it as State) }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyNoMoreInteractions() {
|
||||
verifyNoMoreInteractions(mState1, mState2, mState3, mState4, mState5, mState6)
|
||||
}
|
||||
|
||||
private fun processMessage(what: Int, toState: State?) {
|
||||
mSM.processMessage(what, ARG_1, ARG_2, toState)
|
||||
}
|
||||
|
||||
private fun verifyMessageProcessedBy(what: Int, vararg processedStates: State) {
|
||||
for (state in processedStates) {
|
||||
// InOrder.verify can't check the Message content here because SyncSM will recycle the
|
||||
// message after it's been processed. SyncSM reuses the same Message instance for all
|
||||
// messages it processes. So, if using InOrder.verify to verify the content of a message
|
||||
// after SyncSM has processed it, the content would be wrong.
|
||||
mInOrder.verify(state).processMessage(any())
|
||||
val (processedState, msgWhat) = mMsgProcessedResults.remove()
|
||||
assertEquals(state, processedState)
|
||||
assertEquals(what, msgWhat)
|
||||
}
|
||||
assertTrue(mMsgProcessedResults.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInitialState() {
|
||||
// mState1 -> initial
|
||||
// |
|
||||
// mState2
|
||||
mAllStates.add(StateInfo(mState1, null))
|
||||
mAllStates.add(StateInfo(mState2, mState1))
|
||||
mSM.addAllStates(mAllStates)
|
||||
|
||||
mSM.start(mState1)
|
||||
mInOrder.verify(mState1).enter()
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStartFromLeafState() {
|
||||
// mState1 -> initial
|
||||
// |
|
||||
// mState2
|
||||
// |
|
||||
// mState3
|
||||
mAllStates.add(StateInfo(mState1, null))
|
||||
mAllStates.add(StateInfo(mState2, mState1))
|
||||
mAllStates.add(StateInfo(mState3, mState2))
|
||||
mSM.addAllStates(mAllStates)
|
||||
|
||||
mSM.start(mState3)
|
||||
mInOrder.verify(mState1).enter()
|
||||
mInOrder.verify(mState2).enter()
|
||||
mInOrder.verify(mState3).enter()
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
|
||||
private fun verifyStart() {
|
||||
mSM.addAllStates(mAllStates)
|
||||
mSM.start(mState1)
|
||||
mInOrder.verify(mState1).enter()
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
|
||||
fun addState(state: State, parent: State? = null) {
|
||||
mAllStates.add(StateInfo(state, parent))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddState() {
|
||||
// Add duplicated states.
|
||||
mAllStates.add(StateInfo(mState1, null))
|
||||
mAllStates.add(StateInfo(mState1, null))
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
mSM.addAllStates(mAllStates)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProcessMessage() {
|
||||
// mState1
|
||||
// |
|
||||
// mState2
|
||||
addState(mState1)
|
||||
addState(mState2, mState1)
|
||||
verifyStart()
|
||||
|
||||
processMessage(MSG_1, null)
|
||||
verifyMessageProcessedBy(MSG_1, mState1)
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoStates() {
|
||||
// mState1 <-initial, mState2
|
||||
addState(mState1)
|
||||
addState(mState2)
|
||||
verifyStart()
|
||||
|
||||
// Test transition to mState2
|
||||
processMessage(MSG_1, mState2)
|
||||
verifyMessageProcessedBy(MSG_1, mState1)
|
||||
mInOrder.verify(mState1).exit()
|
||||
mInOrder.verify(mState2).enter()
|
||||
verifyNoMoreInteractions()
|
||||
|
||||
// If set destState to mState2 (current state), no state transition.
|
||||
processMessage(MSG_2, mState2)
|
||||
verifyMessageProcessedBy(MSG_2, mState2)
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoStateTrees() {
|
||||
// mState1 -> initial mState4
|
||||
// / \ / \
|
||||
// mState2 mState3 mState5 mState6
|
||||
addState(mState1)
|
||||
addState(mState2, mState1)
|
||||
addState(mState3, mState1)
|
||||
addState(mState4)
|
||||
addState(mState5, mState4)
|
||||
addState(mState6, mState4)
|
||||
verifyStart()
|
||||
|
||||
// mState1 -> current mState4
|
||||
// / \ / \
|
||||
// mState2 mState3 -> dest mState5 mState6
|
||||
processMessage(MSG_1, mState3)
|
||||
verifyMessageProcessedBy(MSG_1, mState1)
|
||||
mInOrder.verify(mState3).enter()
|
||||
verifyNoMoreInteractions()
|
||||
|
||||
// mState1 mState4
|
||||
// / \ / \
|
||||
// dest <- mState2 mState3 -> current mState5 mState6
|
||||
processMessage(MSG_1, mState2)
|
||||
verifyMessageProcessedBy(MSG_1, mState3, mState1)
|
||||
mInOrder.verify(mState3).exit()
|
||||
mInOrder.verify(mState2).enter()
|
||||
verifyNoMoreInteractions()
|
||||
|
||||
// mState1 mState4
|
||||
// / \ / \
|
||||
// current <- mState2 mState3 mState5 mState6 -> dest
|
||||
processMessage(MSG_2, mState6)
|
||||
verifyMessageProcessedBy(MSG_2, mState2)
|
||||
mInOrder.verify(mState2).exit()
|
||||
mInOrder.verify(mState1).exit()
|
||||
mInOrder.verify(mState4).enter()
|
||||
mInOrder.verify(mState6).enter()
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiDepthTransition() {
|
||||
// mState1 -> current
|
||||
// | \
|
||||
// mState2 mState6
|
||||
// | \ |
|
||||
// mState3 mState5 mState7
|
||||
// |
|
||||
// mState4
|
||||
addState(mState1)
|
||||
addState(mState2, mState1)
|
||||
addState(mState6, mState1)
|
||||
addState(mState3, mState2)
|
||||
addState(mState5, mState2)
|
||||
addState(mState7, mState6)
|
||||
addState(mState4, mState3)
|
||||
verifyStart()
|
||||
|
||||
// mState1 -> current
|
||||
// | \
|
||||
// mState2 mState6
|
||||
// | \ |
|
||||
// mState3 mState5 mState7
|
||||
// |
|
||||
// mState4 -> dest
|
||||
processMessage(MSG_1, mState4)
|
||||
verifyMessageProcessedBy(MSG_1, mState1)
|
||||
mInOrder.verify(mState2).enter()
|
||||
mInOrder.verify(mState3).enter()
|
||||
mInOrder.verify(mState4).enter()
|
||||
verifyNoMoreInteractions()
|
||||
|
||||
// mState1
|
||||
// / \
|
||||
// mState2 mState6
|
||||
// | \ \
|
||||
// mState3 mState5 -> dest mState7
|
||||
// |
|
||||
// mState4 -> current
|
||||
processMessage(MSG_1, mState5)
|
||||
verifyMessageProcessedBy(MSG_1, mState4, mState3, mState2, mState1)
|
||||
mInOrder.verify(mState4).exit()
|
||||
mInOrder.verify(mState3).exit()
|
||||
mInOrder.verify(mState5).enter()
|
||||
verifyNoMoreInteractions()
|
||||
|
||||
// mState1
|
||||
// / \
|
||||
// mState2 mState6
|
||||
// | \ \
|
||||
// mState3 mState5 -> current mState7 -> dest
|
||||
// |
|
||||
// mState4
|
||||
processMessage(MSG_2, mState7)
|
||||
verifyMessageProcessedBy(MSG_2, mState5, mState2)
|
||||
mInOrder.verify(mState5).exit()
|
||||
mInOrder.verify(mState2).exit()
|
||||
mInOrder.verify(mState6).enter()
|
||||
mInOrder.verify(mState7).enter()
|
||||
verifyNoMoreInteractions()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// ktlint does not allow annotating function argument literals inline. Disable the specific rule
|
||||
// since this negatively affects readability.
|
||||
@file:Suppress("ktlint:standard:comment-wrapping")
|
||||
|
||||
package com.android.server.ethernet
|
||||
|
||||
import android.content.Context
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkProvider
|
||||
import android.net.NetworkProvider.NetworkOfferCallback
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.test.TestLooper
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.testutils.DevSdkIgnoreRule
|
||||
import com.android.testutils.DevSdkIgnoreRunner
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
private const val IFACE = "eth0"
|
||||
private val CAPS = NetworkCapabilities.Builder().build()
|
||||
|
||||
@SmallTest
|
||||
@RunWith(DevSdkIgnoreRunner::class)
|
||||
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
|
||||
class EthernetInterfaceStateMachineTest {
|
||||
private lateinit var looper: TestLooper
|
||||
private lateinit var handler: Handler
|
||||
private lateinit var ifaceState: EthernetInterfaceStateMachine
|
||||
|
||||
@Mock private lateinit var context: Context
|
||||
@Mock private lateinit var provider: NetworkProvider
|
||||
@Mock private lateinit var deps: EthernetNetworkFactory.Dependencies
|
||||
|
||||
// There seems to be no (obvious) way to force execution of @Before and @Test annotation on the
|
||||
// same thread. Since SyncStateMachine requires all interactions to be called from the same
|
||||
// thread that is provided at construction time (in this case, the thread that TestLooper() is
|
||||
// called on), setUp() must be called directly from the @Test method.
|
||||
// TODO: find a way to fix this in the test runner.
|
||||
fun setUp() {
|
||||
looper = TestLooper()
|
||||
handler = Handler(looper.looper)
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
ifaceState = EthernetInterfaceStateMachine(IFACE, handler, context, CAPS, provider, deps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateLinkState_networkOfferRegisteredAndRetracted() {
|
||||
setUp()
|
||||
|
||||
ifaceState.updateLinkState(/* up= */ true)
|
||||
|
||||
// link comes up: validate the NetworkOffer is registered and capture callback object.
|
||||
val inOrder = inOrder(provider)
|
||||
val networkOfferCb = ArgumentCaptor.forClass(NetworkOfferCallback::class.java).also {
|
||||
inOrder.verify(provider).registerNetworkOffer(any(), any(), any(), it.capture())
|
||||
}.value
|
||||
|
||||
ifaceState.updateLinkState(/* up */ false)
|
||||
|
||||
// link goes down: validate the NetworkOffer is retracted
|
||||
inOrder.verify(provider).unregisterNetworkOffer(eq(networkOfferCb))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue