Basic dependency injection scope with Guice
Setup the basic scope for Guice dependency injection in
a tradefed invocation scope.
Test: unit tests
Bug: 77487632
Change-Id: I52bc80718939ad4fa9736d07cf147c4615f45dfb
diff --git a/.classpath b/.classpath
index 3488449..d8e2fed 100644
--- a/.classpath
+++ b/.classpath
@@ -20,5 +20,6 @@
<classpathentry kind="var" path="TRADEFED_ROOT/external/error_prone/error_prone/error_prone_annotations-2.2.0.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/LongevityRunner"/>
<classpathentry kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/junit-params-host_intermediates/classes.jar"/>
+ <classpathentry kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guice_intermediates/classes.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/Android.mk b/Android.mk
index f58bb0b..4bcaeb6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -52,7 +52,7 @@
LOCAL_MODULE := tradefed
-LOCAL_STATIC_JAVA_LIBRARIES := junit-host junit-params-host kxml2-2.3.0 jline-1.0 tf-remote-client commons-compress-prebuilt host-libprotobuf-java-full tradefed-protos error_prone_annotations-2.0.18 longevity-host-lib gson-prebuilt-jar
+LOCAL_STATIC_JAVA_LIBRARIES := junit-host junit-params-host kxml2-2.3.0 jline-1.0 tf-remote-client commons-compress-prebuilt host-libprotobuf-java-full tradefed-protos error_prone_annotations-2.0.18 longevity-host-lib gson-prebuilt-jar guice
# emmalib is only a runtime dependency if generating code coverage reporters,
# not a compile time dependency
diff --git a/src/com/android/tradefed/guice/InvocationScope.java b/src/com/android/tradefed/guice/InvocationScope.java
new file mode 100644
index 0000000..5b11851
--- /dev/null
+++ b/src/com/android/tradefed/guice/InvocationScope.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2018 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.tradefed.guice;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.android.tradefed.config.IConfiguration;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.Scopes;
+
+import java.util.Map;
+
+/**
+ * Scopes a single Tradefed invocation.
+ *
+ * <p>The scope can be initialized with one or more seed values by calling <code>seed(key, value)
+ * </code> before the injector will be called upon to provide for this key. A typical use is for a
+ * test invocation to enter/exit the scope, representing an invocation Scope, and seed configuration
+ * objects. For each key inserted with seed(), you must include a corresponding binding:
+ *
+ * <pre><code>
+ * bind(key)
+ * .toProvider(SimpleScope.<key.class>seededKeyProvider())
+ * .in(InvocationScoped.class);
+ * </code></pre>
+ *
+ * FIXME: Possibly handle multi objects (like lists).
+ */
+public class InvocationScope implements Scope {
+
+ public InvocationScope() {}
+
+ private static final Provider<Object> SEEDED_KEY_PROVIDER =
+ new Provider<Object>() {
+ @Override
+ public Object get() {
+ throw new IllegalStateException(
+ "If you got here then it means that"
+ + " your code asked for scoped object which should have been"
+ + " explicitly seeded in this scope by calling"
+ + " SimpleScope.seed(), but was not.");
+ }
+ };
+
+ private static InvocationScope sDefaultInstance = null;
+
+ public static InvocationScope getDefault() {
+ if (sDefaultInstance == null) {
+ sDefaultInstance = new InvocationScope();
+ }
+ return sDefaultInstance;
+ }
+
+ private final ThreadLocal<Map<Key<?>, Object>> values = new ThreadLocal<Map<Key<?>, Object>>();
+
+ /** Start marking the scope of the Tradefed Invocation. */
+ public void enter() {
+ checkState(values.get() == null, "A scoping block is already in progress");
+ values.set(Maps.<Key<?>, Object>newHashMap());
+ }
+
+ /** Mark the end of the scope for the Tradefed Invocation. */
+ public void exit() {
+ checkState(values.get() != null, "No scoping block in progress");
+ values.remove();
+ }
+
+ /**
+ * Interface init between Tradefed and Guice: This is the place where TF object are seeded to
+ * the invocation scope to be used.
+ *
+ * @param config The Tradefed configuration.
+ */
+ public void seedConfiguration(IConfiguration config) {
+ // First seed the configuration itself
+ seed(IConfiguration.class, config);
+ // Then inject the seeded objects to the configuration.
+ injectToConfig(config);
+ }
+
+ private void injectToConfig(IConfiguration config) {
+ Injector injector = Guice.createInjector(new InvocationScopeModule(this));
+
+ // TODO: inject to TF objects that could require it.
+ // Do injection against current test objects: This allows to pass the injector
+ for (Object obj : config.getTests()) {
+ injector.injectMembers(obj);
+ }
+ }
+
+ /**
+ * Seed a key/value that will be available during the TF invocation scope to be used.
+ *
+ * @param key the key used to represent the object.
+ * @param value The actual object that will be available during the invocation.
+ */
+ public <T> void seed(Key<T> key, T value) {
+ Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
+ checkState(
+ !scopedObjects.containsKey(key),
+ "A value for the key %s was "
+ + "already seeded in this scope. Old value: %s New value: %s",
+ key,
+ scopedObjects.get(key),
+ value);
+ scopedObjects.put(key, value);
+ }
+
+ /**
+ * Seed a key/value that will be available during the TF invocation scope to be used.
+ *
+ * @param clazz the Class used to represent the object.
+ * @param value The actual object that will be available during the invocation.
+ */
+ public <T> void seed(Class<T> clazz, T value) {
+ seed(Key.get(clazz), value);
+ }
+
+ @Override
+ public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
+ return new Provider<T>() {
+ @Override
+ public T get() {
+ Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
+
+ @SuppressWarnings("unchecked")
+ T current = (T) scopedObjects.get(key);
+ if (current == null && !scopedObjects.containsKey(key)) {
+ current = unscoped.get();
+
+ // don't remember proxies; these exist only to serve circular dependencies
+ if (Scopes.isCircularProxy(current)) {
+ return current;
+ }
+
+ scopedObjects.put(key, current);
+ }
+ return current;
+ }
+ };
+ }
+
+ private <T> Map<Key<?>, Object> getScopedObjectMap(Key<T> key) {
+ Map<Key<?>, Object> scopedObjects = values.get();
+ if (scopedObjects == null) {
+ throw new OutOfScopeException("Cannot access " + key + " outside of a scoping block");
+ }
+ return scopedObjects;
+ }
+
+ /**
+ * Returns a provider that always throws exception complaining that the object in question must
+ * be seeded before it can be injected.
+ *
+ * @return typed provider
+ */
+ @SuppressWarnings({"unchecked"})
+ public static <T> Provider<T> seededKeyProvider() {
+ return (Provider<T>) SEEDED_KEY_PROVIDER;
+ }
+}
diff --git a/src/com/android/tradefed/guice/InvocationScopeModule.java b/src/com/android/tradefed/guice/InvocationScopeModule.java
new file mode 100644
index 0000000..a718de2
--- /dev/null
+++ b/src/com/android/tradefed/guice/InvocationScopeModule.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.tradefed.guice;
+
+import com.android.tradefed.config.IConfiguration;
+
+import com.google.inject.AbstractModule;
+
+/**
+ * Guice module that can be used anywhere in a TF invocation to requests the Guice-Tradefed
+ * supported objects.
+ */
+public class InvocationScopeModule extends AbstractModule {
+
+ private InvocationScope mScope;
+
+ public InvocationScopeModule() {
+ this(InvocationScope.getDefault());
+ }
+
+ public InvocationScopeModule(InvocationScope scope) {
+ mScope = scope;
+ }
+
+ @Override
+ public void configure() {
+ // Tell Guice about the scope
+ bindScope(InvocationScoped.class, mScope);
+
+ // Make our scope instance injectable
+ bind(InvocationScope.class).toInstance(mScope);
+
+ // IConfiguration is a supported Guice-Tradefed object.
+ bind(IConfiguration.class)
+ .toProvider(InvocationScope.<IConfiguration>seededKeyProvider())
+ .in(InvocationScoped.class);
+ }
+}
diff --git a/src/com/android/tradefed/guice/InvocationScoped.java b/src/com/android/tradefed/guice/InvocationScoped.java
new file mode 100644
index 0000000..4cb4f5b
--- /dev/null
+++ b/src/com/android/tradefed/guice/InvocationScoped.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 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.tradefed.guice;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.ScopeAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@ScopeAnnotation
+public @interface InvocationScoped {}
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 0a7f246..949fa8d 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -28,6 +28,7 @@
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.device.TestDeviceState;
+import com.android.tradefed.guice.InvocationScope;
import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
import com.android.tradefed.invoker.shard.ShardBuildCloner;
import com.android.tradefed.log.ILeveledLogOutput;
@@ -607,6 +608,12 @@
new LogSaverResultForwarder(config.getLogSaver(), allListeners);
IInvocationExecution invocationPath =
createInvocationExec(config.getConfigurationDescription().shouldUseSandbox());
+
+ // Create the Guice scope
+ InvocationScope scope = getInvocationScope();
+ scope.enter();
+ // Seed our TF objects to the Guice scope
+ scope.seedConfiguration(config);
try {
mStatus = "fetching build";
config.getLogOutput().init();
@@ -657,7 +664,7 @@
} catch (IOException e) {
CLog.e(e);
} finally {
-
+ scope.exit();
// Ensure build infos are always cleaned up at the end of invocation.
invocationPath.cleanUpBuilds(context, config);
@@ -675,6 +682,12 @@
}
}
+ /** Returns the current {@link InvocationScope}. */
+ @VisibleForTesting
+ InvocationScope getInvocationScope() {
+ return InvocationScope.getDefault();
+ }
+
/**
* Helper to set the exit code. Exposed for testing.
*/
diff --git a/tests/.classpath b/tests/.classpath
index 989a916..d0579b3 100644
--- a/tests/.classpath
+++ b/tests/.classpath
@@ -19,5 +19,6 @@
<classpathentry kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/tradefed-protos_intermediates/classes.jar"/>
<classpathentry kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/truth/truth-0.28.jar"/>
<classpathentry kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/junit-params-host_intermediates/classes.jar"/>
+ <classpathentry kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guice_intermediates/classes.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 86e9cb8..930f69d 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -84,6 +84,7 @@
import com.android.tradefed.device.metric.ScheduledDeviceMetricCollectorTest;
import com.android.tradefed.device.metric.TemperatureCollectorTest;
import com.android.tradefed.device.metric.TraceMetricCollectorTest;
+import com.android.tradefed.guice.InvocationScopeTest;
import com.android.tradefed.invoker.InvocationContextTest;
import com.android.tradefed.invoker.InvocationExecutionTest;
import com.android.tradefed.invoker.SandboxedInvocationExecutionTest;
@@ -377,6 +378,9 @@
TemperatureCollectorTest.class,
TraceMetricCollectorTest.class,
+ // Guice
+ InvocationScopeTest.class,
+
// invoker
InvocationContextTest.class,
InvocationExecutionTest.class,
diff --git a/tests/src/com/android/tradefed/guice/InvocationScopeTest.java b/tests/src/com/android/tradefed/guice/InvocationScopeTest.java
new file mode 100644
index 0000000..0ae5574
--- /dev/null
+++ b/tests/src/com/android/tradefed/guice/InvocationScopeTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 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.tradefed.guice;
+
+import static org.junit.Assert.*;
+
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.targetprep.multi.StubMultiTargetPreparer;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+/** Unit tests for {@link InvocationScope}. */
+@RunWith(JUnit4.class)
+public class InvocationScopeTest {
+
+ /** Test class to check that object are properly seeded. */
+ public static class InjectedClass implements IRemoteTest {
+
+ public Injector mInjector;
+ public IConfiguration mConfiguration;
+
+ @Inject
+ public void setInjector(Injector injector) {
+ mInjector = injector;
+ }
+
+ @Inject
+ public void setConfiguration(IConfiguration configuration) {
+ mConfiguration = configuration;
+ }
+
+ @Override
+ public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ // Do nothing
+ }
+ }
+
+ private IConfiguration mConfiguration;
+
+ @Before
+ public void setUp() {
+ mConfiguration = new Configuration("test", "test");
+ }
+
+ /** Test that the injection and seed object are available in the scope. */
+ @Test
+ public void testInjection() {
+ InjectedClass test = new InjectedClass();
+ mConfiguration.setTest(test);
+ mConfiguration.setMultiTargetPreparers(Arrays.asList(new StubMultiTargetPreparer()));
+ InvocationScope scope = new InvocationScope();
+ scope.enter();
+ try {
+ scope.seedConfiguration(mConfiguration);
+ assertNotNull(test.mInjector);
+ assertNotNull(test.mConfiguration);
+ assertEquals(mConfiguration, test.mConfiguration);
+ } finally {
+ scope.exit();
+ }
+ }
+}
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index 1aad666..c094f57 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -15,6 +15,8 @@
*/
package com.android.tradefed.invoker;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import com.android.tradefed.build.IBuildProvider;
@@ -23,10 +25,13 @@
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.guice.InvocationScope;
import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
import com.android.tradefed.log.ILogRegistry;
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
import org.junit.Before;
import org.junit.Test;
@@ -71,6 +76,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
mConfig = new Configuration("test", "test");
mContext = new InvocationContext();
@@ -87,6 +98,10 @@
mConfig.setLogSaver(mMockLogSaver);
mConfig.setBuildProvider(mMockProvider);
+ doReturn(new LogFile("file", "url", LogDataType.TEXT))
+ .when(mMockLogSaver)
+ .saveLogData(any(), any(), any());
+
mInvocation.invoke(mContext, mConfig, mMockRescheduler, mMockListener);
// Ensure that in sandbox we don't download again.
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index a5149f7..1e4b56d 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -26,6 +26,7 @@
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.guice.InvocationScope;
import com.android.tradefed.invoker.shard.IShardHelper;
import com.android.tradefed.invoker.shard.ShardHelper;
import com.android.tradefed.log.ILeveledLogOutput;
@@ -94,6 +95,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
}
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index 68c4fa3..127986f 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -50,6 +50,7 @@
import com.android.tradefed.device.metric.BaseDeviceMetricCollector;
import com.android.tradefed.device.metric.DeviceMetricData;
import com.android.tradefed.device.metric.IMetricCollector;
+import com.android.tradefed.guice.InvocationScope;
import com.android.tradefed.invoker.shard.IShardHelper;
import com.android.tradefed.invoker.shard.ShardHelper;
import com.android.tradefed.invoker.shard.StrictShardHelper;
@@ -246,6 +247,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
}
@@ -863,6 +870,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
String[] commandLine = {"config", "arg"};
int shardCount = 10;
@@ -947,6 +960,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
String[] commandLine = {"config", "arg"};
int shardCount = 10;
@@ -1613,6 +1632,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(new HashSet<>());
@@ -1677,6 +1702,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
IRemoteTest test = EasyMock.createNiceMock(IRemoteTest.class);
@@ -1754,6 +1785,12 @@
protected void setExitCode(ExitCode code, Throwable stack) {
// empty on purpose
}
+
+ @Override
+ InvocationScope getInvocationScope() {
+ // Avoid re-entry in the current TF invocation scope for unit tests.
+ return new InvocationScope();
+ }
};
mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
IRemoteTest test = EasyMock.createNiceMock(IRemoteTest.class);