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);