diff --git a/services/robotests/Android.mk b/services/robotests/Android.mk index 114929ccecfd7..b5e4af78098fd 100644 --- a/services/robotests/Android.mk +++ b/services/robotests/Android.mk @@ -40,15 +40,15 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) # Include the testing libraries (JUnit4 + Robolectric libs). LOCAL_STATIC_JAVA_LIBRARIES := \ + platform-robolectric-android-all-stubs \ android-support-test \ mockito-robolectric-prebuilt \ platform-test-annotations \ truth-prebuilt -# TODO(b/69254249): Migrate to Robolectric 3.4.2 LOCAL_JAVA_LIBRARIES := \ junit \ - platform-robolectric-3.1.1-prebuilt + platform-robolectric-3.5.1-prebuilt LOCAL_INSTRUMENTATION_FOR := FrameworksServicesLib LOCAL_MODULE := FrameworksServicesRoboTests @@ -73,5 +73,4 @@ LOCAL_TEST_PACKAGE := FrameworksServicesLib LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))backup/java -# TODO(b/69254249): Migrate to Robolectric 3.4.2 -include prebuilts/misc/common/robolectric/3.1.1/run_robotests.mk +include prebuilts/misc/common/robolectric/3.5.1/run_robotests.mk diff --git a/services/robotests/src/com/android/server/backup/TransportManagerTest.java b/services/robotests/src/com/android/server/backup/TransportManagerTest.java index 3d5851fc08a74..202f8c71b6df6 100644 --- a/services/robotests/src/com/android/server/backup/TransportManagerTest.java +++ b/services/robotests/src/com/android/server/backup/TransportManagerTest.java @@ -19,6 +19,7 @@ package com.android.server.backup; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import static org.robolectric.shadow.api.Shadow.extract; import android.app.backup.BackupManager; import android.content.ComponentName; @@ -31,24 +32,25 @@ import android.os.IBinder; import android.platform.test.annotations.Presubmit; import com.android.server.backup.testing.BackupTransportStub; -import com.android.server.backup.testing.DefaultPackageManagerWithQueryIntentServicesAsUser; import com.android.server.backup.testing.ShadowBackupTransportStub; import com.android.server.backup.testing.ShadowContextImplForBackup; +import com.android.server.backup.testing.ShadowPackageManagerForBackup; import com.android.server.backup.testing.TransportBoundListenerStub; import com.android.server.backup.testing.TransportReadyCallbackStub; import com.android.server.backup.transport.TransportClient; +import com.android.server.testing.FrameworkRobolectricTestRunner; +import com.android.server.testing.SystemLoaderClasses; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.res.builder.RobolectricPackageManager; import org.robolectric.shadows.ShadowLog; import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowPackageManager; import java.util.ArrayList; import java.util.Arrays; @@ -56,15 +58,17 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -@RunWith(RobolectricTestRunner.class) +@RunWith(FrameworkRobolectricTestRunner.class) @Config( manifest = Config.NONE, - sdk = 23, + sdk = 26, shadows = { ShadowContextImplForBackup.class, - ShadowBackupTransportStub.class + ShadowBackupTransportStub.class, + ShadowPackageManagerForBackup.class } ) +@SystemLoaderClasses({TransportManager.class}) @Presubmit public class TransportManagerTest { private static final String PACKAGE_NAME = "some.package.name"; @@ -73,7 +77,7 @@ public class TransportManagerTest { private TransportInfo mTransport1; private TransportInfo mTransport2; - private RobolectricPackageManager mPackageManager; + private ShadowPackageManager mPackageManagerShadow; private final TransportBoundListenerStub mTransportBoundListenerStub = new TransportBoundListenerStub(true); @@ -86,9 +90,10 @@ public class TransportManagerTest { MockitoAnnotations.initMocks(this); ShadowLog.stream = System.out; - mPackageManager = new DefaultPackageManagerWithQueryIntentServicesAsUser( - RuntimeEnvironment.getAppResourceLoader()); - RuntimeEnvironment.setRobolectricPackageManager(mPackageManager); + + mPackageManagerShadow = + (ShadowPackageManagerForBackup) + extract(RuntimeEnvironment.application.getPackageManager()); mTransport1 = new TransportInfo(PACKAGE_NAME, "transport1.name"); mTransport2 = new TransportInfo(PACKAGE_NAME, "transport2.name"); @@ -539,7 +544,7 @@ public class TransportManagerTest { packageInfo.applicationInfo = new ApplicationInfo(); packageInfo.applicationInfo.privateFlags = flags; - mPackageManager.addPackage(packageInfo); + mPackageManagerShadow.addPackage(packageInfo); List transportsInfo = new ArrayList<>(); for (TransportInfo transport : transports) { @@ -553,7 +558,7 @@ public class TransportManagerTest { Intent intent = new Intent(TransportManager.SERVICE_ACTION_TRANSPORT_HOST); intent.setPackage(packageName); - mPackageManager.addResolveInfoForIntent(intent, transportsInfo); + mPackageManagerShadow.addResolveInfoForIntent(intent, transportsInfo); } private TransportManager createTransportManagerAndSetUpTransports( diff --git a/services/robotests/src/com/android/server/backup/testing/DefaultPackageManagerWithQueryIntentServicesAsUser.java b/services/robotests/src/com/android/server/backup/testing/ShadowPackageManagerForBackup.java similarity index 69% rename from services/robotests/src/com/android/server/backup/testing/DefaultPackageManagerWithQueryIntentServicesAsUser.java rename to services/robotests/src/com/android/server/backup/testing/ShadowPackageManagerForBackup.java index 5a0967ba24209..b64b59d238d16 100644 --- a/services/robotests/src/com/android/server/backup/testing/DefaultPackageManagerWithQueryIntentServicesAsUser.java +++ b/services/robotests/src/com/android/server/backup/testing/ShadowPackageManagerForBackup.java @@ -16,28 +16,22 @@ package com.android.server.backup.testing; +import android.app.ApplicationPackageManager; import android.content.Intent; import android.content.pm.ResolveInfo; -import org.robolectric.res.ResourceLoader; -import org.robolectric.res.builder.DefaultPackageManager; +import org.robolectric.annotation.Implements; +import org.robolectric.shadows.ShadowApplicationPackageManager; import java.util.List; /** * Implementation of PackageManager for Robolectric which handles queryIntentServicesAsUser(). */ -public class DefaultPackageManagerWithQueryIntentServicesAsUser extends - DefaultPackageManager { - - /* package */ - public DefaultPackageManagerWithQueryIntentServicesAsUser( - ResourceLoader appResourceLoader) { - super(appResourceLoader); - } - +@Implements(value = ApplicationPackageManager.class, inheritImplementationMethods = true) +public class ShadowPackageManagerForBackup extends ShadowApplicationPackageManager { @Override public List queryIntentServicesAsUser(Intent intent, int flags, int userId) { - return super.queryIntentServices(intent, flags); + return queryIntentServices(intent, flags); } } diff --git a/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java b/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java index 4a3277f2be020..4462d2a01c761 100644 --- a/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java +++ b/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java @@ -17,6 +17,7 @@ package com.android.server.backup.transport; import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -37,31 +38,34 @@ import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import com.android.internal.backup.IBackupTransport; +import com.android.server.backup.TransportManager; +import com.android.server.testing.FrameworkRobolectricTestRunner; +import com.android.server.testing.SystemLoaderClasses; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE, sdk = 23) +@RunWith(FrameworkRobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 26) +@SystemLoaderClasses({TransportManager.class, TransportClient.class}) @Presubmit public class TransportClientTest { private static final String PACKAGE_NAME = "some.package.name"; - private static final ComponentName TRANSPORT_COMPONENT = - new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport"); - private static final String TRANSPORT_DIR_NAME = TRANSPORT_COMPONENT.toString(); @Mock private Context mContext; @Mock private TransportConnectionListener mTransportConnectionListener; @Mock private TransportConnectionListener mTransportConnectionListener2; @Mock private IBackupTransport.Stub mIBackupTransport; private TransportClient mTransportClient; + private ComponentName mTransportComponent; + private String mTransportDirName; private Intent mBindIntent; private ShadowLooper mShadowLooper; @@ -71,13 +75,16 @@ public class TransportClientTest { Looper mainLooper = Looper.getMainLooper(); mShadowLooper = shadowOf(mainLooper); - mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(TRANSPORT_COMPONENT); + mTransportComponent = + new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport"); + mTransportDirName = mTransportComponent.toString(); + mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent); mTransportClient = new TransportClient( mContext, mBindIntent, - TRANSPORT_COMPONENT, - TRANSPORT_DIR_NAME, + mTransportComponent, + mTransportDirName, "1", new Handler(mainLooper)); @@ -91,12 +98,12 @@ public class TransportClientTest { @Test public void testGetTransportDirName_returnsTransportDirName() { - assertThat(mTransportClient.getTransportDirName()).isEqualTo(TRANSPORT_DIR_NAME); + assertThat(mTransportClient.getTransportDirName()).isEqualTo(mTransportDirName); } @Test public void testGetTransportComponent_returnsTransportComponent() { - assertThat(mTransportClient.getTransportComponent()).isEqualTo(TRANSPORT_COMPONENT); + assertThat(mTransportClient.getTransportComponent()).isEqualTo(mTransportComponent); } @Test @@ -117,7 +124,7 @@ public class TransportClientTest { // Simulate framework connecting ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); - connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + connection.onServiceConnected(mTransportComponent, mIBackupTransport); mShadowLooper.runToEndOfTasks(); verify(mTransportConnectionListener) @@ -132,7 +139,7 @@ public class TransportClientTest { mTransportClient.connectAsync(mTransportConnectionListener2, "caller2"); - connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + connection.onServiceConnected(mTransportComponent, mIBackupTransport); mShadowLooper.runToEndOfTasks(); verify(mTransportConnectionListener) @@ -145,7 +152,7 @@ public class TransportClientTest { public void testConnectAsync_whenAlreadyConnected_callsListener() throws Exception { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); - connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + connection.onServiceConnected(mTransportComponent, mIBackupTransport); mTransportClient.connectAsync(mTransportConnectionListener2, "caller2"); @@ -190,8 +197,8 @@ public class TransportClientTest { throws Exception { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); - connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); - connection.onServiceDisconnected(TRANSPORT_COMPONENT); + connection.onServiceConnected(mTransportComponent, mIBackupTransport); + connection.onServiceDisconnected(mTransportComponent); mTransportClient.connectAsync(mTransportConnectionListener2, "caller1"); @@ -204,9 +211,9 @@ public class TransportClientTest { throws Exception { mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); - connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); - connection.onServiceDisconnected(TRANSPORT_COMPONENT); - connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + connection.onServiceConnected(mTransportComponent, mIBackupTransport); + connection.onServiceDisconnected(mTransportComponent); + connection.onServiceConnected(mTransportComponent, mIBackupTransport); mTransportClient.connectAsync(mTransportConnectionListener2, "caller1"); @@ -221,7 +228,7 @@ public class TransportClientTest { mTransportClient.connectAsync(mTransportListener, "caller"); ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); - connection.onBindingDied(TRANSPORT_COMPONENT); + connection.onBindingDied(mTransportComponent); mShadowLooper.runToEndOfTasks(); verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient)); @@ -235,7 +242,7 @@ public class TransportClientTest { mTransportClient.connectAsync(mTransportListener2, "caller2"); - connection.onBindingDied(TRANSPORT_COMPONENT); + connection.onBindingDied(mTransportComponent); mShadowLooper.runToEndOfTasks(); verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient)); diff --git a/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java b/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java new file mode 100644 index 0000000000000..78ac4ed92788a --- /dev/null +++ b/services/robotests/src/com/android/server/testing/FrameworkRobolectricTestRunner.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2017 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.testing; + +import com.google.common.collect.ImmutableSet; + +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.internal.SandboxFactory; +import org.robolectric.internal.SdkEnvironment; +import org.robolectric.internal.bytecode.InstrumentationConfiguration; +import org.robolectric.internal.bytecode.SandboxClassLoader; +import org.robolectric.util.Util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.URL; +import java.util.Arrays; +import java.util.Set; + +import javax.annotation.Nonnull; + +/** + * HACK + * Robolectric loads up Android environment from prebuilt android jars before running a method. + * These jars are versioned according to the SDK level configured for the method (or class). The + * jars represent a snapshot of the Android APIs in that SDK level. For Robolectric tests that are + * testing Android components themselves we don't want certain classes (usually the + * class-under-test) to be loaded from the prebuilt jar, we want it instead to be loaded from the + * dependencies of our test target, i.e. the system class loader. That way we can write tests + * against the actual classes that are in the tree, not a past version of them. Ideally we would + * have a locally built jar referenced by Robolectric, but until that happens one can use this + * class. + * This class reads the {@link SystemLoaderClasses} annotation on test classes and for each class + * in that annotation value it will bypass the android jar and load it from the system class loader. + * Allowing the test to test the actual class in the tree. + * + * Implementation note: One could think about overriding + * {@link RobolectricTestRunner#createClassLoaderConfig(FrameworkMethod)} method and putting the + * classes in the annotation in the {@link InstrumentationConfiguration} list of classes not to + * acquire. Unfortunately, this will not work because we will not be instrumenting the class. + * Instead, we have to load the class bytes from the system class loader but still instrument it, we + * do this by overriding {@link SandboxClassLoader#getByteCode(String)} and loading the class bytes + * from the system class loader if it in the {@link SystemLoaderClasses} annotation. This way the + * {@link SandboxClassLoader} still instruments the class, but it's not loaded from the android jar. + * Finally, we inject the custom class loader in place of the default one. + * + * TODO: Remove this when we are using locally built android jars in the method's environment. + */ +public class FrameworkRobolectricTestRunner extends RobolectricTestRunner { + private final SandboxFactory mSandboxFactory; + + public FrameworkRobolectricTestRunner(Class testClass) throws InitializationError { + super(testClass); + SystemLoaderClasses annotation = testClass.getAnnotation(SystemLoaderClasses.class); + Class[] systemLoaderClasses = + (annotation != null) ? annotation.value() : new Class[0]; + Set systemLoaderClassNames = classesToClassNames(systemLoaderClasses); + mSandboxFactory = new FrameworkSandboxFactory(systemLoaderClassNames); + } + + @Nonnull + @Override + protected SdkEnvironment getSandbox(FrameworkMethod method) { + // HACK: Calling super just to get SdkConfig via sandbox.getSdkConfig(), because + // RobolectricFrameworkMethod, the runtime class of method, is package-protected + SdkEnvironment sandbox = super.getSandbox(method); + return mSandboxFactory.getSdkEnvironment( + createClassLoaderConfig(method), + getJarResolver(), + sandbox.getSdkConfig()); + } + + private static class FrameworkClassLoader extends SandboxClassLoader { + private final Set mSystemLoaderClasses; + + private FrameworkClassLoader( + Set systemLoaderClasses, + ClassLoader systemClassLoader, + InstrumentationConfiguration instrumentationConfig, + URL... urls) { + super(systemClassLoader, instrumentationConfig, urls); + mSystemLoaderClasses = systemLoaderClasses; + } + + @Override + protected byte[] getByteCode(String className) throws ClassNotFoundException { + String classFileName = className.replace('.', '/') + ".class"; + if (shouldLoadFromSystemLoader(className)) { + try (InputStream classByteStream = getResourceAsStream(classFileName)) { + if (classByteStream == null) { + throw new ClassNotFoundException(className); + } + return Util.readBytes(classByteStream); + } catch (IOException e) { + throw new ClassNotFoundException( + "Couldn't load " + className + " from system class loader", e); + } + } + return super.getByteCode(className); + } + + /** + * Classes like com.package.ClassName$InnerClass should also be loaded from the system class + * loader, so we test if the classes in the annotation are prefixes of the class to load. + */ + private boolean shouldLoadFromSystemLoader(String className) { + for (String classNamePrefix : mSystemLoaderClasses) { + if (className.startsWith(classNamePrefix)) { + return true; + } + } + return false; + } + } + + private static class FrameworkSandboxFactory extends SandboxFactory { + private final Set mSystemLoaderClasses; + + private FrameworkSandboxFactory(Set systemLoaderClasses) { + mSystemLoaderClasses = systemLoaderClasses; + } + + @Nonnull + @Override + public ClassLoader createClassLoader( + InstrumentationConfiguration instrumentationConfig, URL... urls) { + return new FrameworkClassLoader( + mSystemLoaderClasses, + ClassLoader.getSystemClassLoader(), + instrumentationConfig, + urls); + } + } + + private static Set classesToClassNames(Class[] classes) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Class classObject : classes) { + builder.add(classObject.getName()); + } + return builder.build(); + } +} diff --git a/services/robotests/src/com/android/server/testing/SystemLoaderClasses.java b/services/robotests/src/com/android/server/testing/SystemLoaderClasses.java new file mode 100644 index 0000000000000..646a413c2667b --- /dev/null +++ b/services/robotests/src/com/android/server/testing/SystemLoaderClasses.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be used in test classes that run with {@link FrameworkRobolectricTestRunner}. + * This will make the classes specified be loaded from the system class loader, NOT from the + * Robolectric android jar. + * + * @see FrameworkRobolectricTestRunner + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface SystemLoaderClasses { + Class[] value() default {}; +}