Bernardo Rufino | 90a8802 | 2017-11-13 17:19:15 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License |
| 15 | */ |
| 16 | |
| 17 | package com.android.server.testing; |
| 18 | |
| 19 | import com.google.common.collect.ImmutableSet; |
| 20 | |
| 21 | import org.junit.runners.model.FrameworkMethod; |
| 22 | import org.junit.runners.model.InitializationError; |
| 23 | import org.robolectric.RobolectricTestRunner; |
| 24 | import org.robolectric.internal.SandboxFactory; |
| 25 | import org.robolectric.internal.SdkEnvironment; |
| 26 | import org.robolectric.internal.bytecode.InstrumentationConfiguration; |
| 27 | import org.robolectric.internal.bytecode.SandboxClassLoader; |
| 28 | import org.robolectric.util.Util; |
| 29 | |
Bernardo Rufino | 90a8802 | 2017-11-13 17:19:15 +0000 | [diff] [blame] | 30 | import java.io.IOException; |
| 31 | import java.io.InputStream; |
Bernardo Rufino | 90a8802 | 2017-11-13 17:19:15 +0000 | [diff] [blame] | 32 | import java.net.URL; |
Bernardo Rufino | 90a8802 | 2017-11-13 17:19:15 +0000 | [diff] [blame] | 33 | import java.util.Set; |
| 34 | |
| 35 | import javax.annotation.Nonnull; |
| 36 | |
| 37 | /** |
| 38 | * HACK |
| 39 | * Robolectric loads up Android environment from prebuilt android jars before running a method. |
| 40 | * These jars are versioned according to the SDK level configured for the method (or class). The |
| 41 | * jars represent a snapshot of the Android APIs in that SDK level. For Robolectric tests that are |
| 42 | * testing Android components themselves we don't want certain classes (usually the |
| 43 | * class-under-test) to be loaded from the prebuilt jar, we want it instead to be loaded from the |
| 44 | * dependencies of our test target, i.e. the system class loader. That way we can write tests |
| 45 | * against the actual classes that are in the tree, not a past version of them. Ideally we would |
| 46 | * have a locally built jar referenced by Robolectric, but until that happens one can use this |
| 47 | * class. |
| 48 | * This class reads the {@link SystemLoaderClasses} annotation on test classes and for each class |
| 49 | * in that annotation value it will bypass the android jar and load it from the system class loader. |
| 50 | * Allowing the test to test the actual class in the tree. |
| 51 | * |
| 52 | * Implementation note: One could think about overriding |
| 53 | * {@link RobolectricTestRunner#createClassLoaderConfig(FrameworkMethod)} method and putting the |
| 54 | * classes in the annotation in the {@link InstrumentationConfiguration} list of classes not to |
| 55 | * acquire. Unfortunately, this will not work because we will not be instrumenting the class. |
| 56 | * Instead, we have to load the class bytes from the system class loader but still instrument it, we |
| 57 | * do this by overriding {@link SandboxClassLoader#getByteCode(String)} and loading the class bytes |
| 58 | * from the system class loader if it in the {@link SystemLoaderClasses} annotation. This way the |
| 59 | * {@link SandboxClassLoader} still instruments the class, but it's not loaded from the android jar. |
| 60 | * Finally, we inject the custom class loader in place of the default one. |
| 61 | * |
| 62 | * TODO: Remove this when we are using locally built android jars in the method's environment. |
| 63 | */ |
| 64 | public class FrameworkRobolectricTestRunner extends RobolectricTestRunner { |
| 65 | private final SandboxFactory mSandboxFactory; |
| 66 | |
| 67 | public FrameworkRobolectricTestRunner(Class<?> testClass) throws InitializationError { |
| 68 | super(testClass); |
| 69 | SystemLoaderClasses annotation = testClass.getAnnotation(SystemLoaderClasses.class); |
| 70 | Class<?>[] systemLoaderClasses = |
| 71 | (annotation != null) ? annotation.value() : new Class<?>[0]; |
| 72 | Set<String> systemLoaderClassNames = classesToClassNames(systemLoaderClasses); |
| 73 | mSandboxFactory = new FrameworkSandboxFactory(systemLoaderClassNames); |
| 74 | } |
| 75 | |
| 76 | @Nonnull |
| 77 | @Override |
| 78 | protected SdkEnvironment getSandbox(FrameworkMethod method) { |
| 79 | // HACK: Calling super just to get SdkConfig via sandbox.getSdkConfig(), because |
| 80 | // RobolectricFrameworkMethod, the runtime class of method, is package-protected |
| 81 | SdkEnvironment sandbox = super.getSandbox(method); |
| 82 | return mSandboxFactory.getSdkEnvironment( |
| 83 | createClassLoaderConfig(method), |
| 84 | getJarResolver(), |
| 85 | sandbox.getSdkConfig()); |
| 86 | } |
| 87 | |
| 88 | private static class FrameworkClassLoader extends SandboxClassLoader { |
| 89 | private final Set<String> mSystemLoaderClasses; |
| 90 | |
| 91 | private FrameworkClassLoader( |
| 92 | Set<String> systemLoaderClasses, |
| 93 | ClassLoader systemClassLoader, |
| 94 | InstrumentationConfiguration instrumentationConfig, |
| 95 | URL... urls) { |
| 96 | super(systemClassLoader, instrumentationConfig, urls); |
| 97 | mSystemLoaderClasses = systemLoaderClasses; |
| 98 | } |
| 99 | |
| 100 | @Override |
| 101 | protected byte[] getByteCode(String className) throws ClassNotFoundException { |
| 102 | String classFileName = className.replace('.', '/') + ".class"; |
| 103 | if (shouldLoadFromSystemLoader(className)) { |
| 104 | try (InputStream classByteStream = getResourceAsStream(classFileName)) { |
| 105 | if (classByteStream == null) { |
| 106 | throw new ClassNotFoundException(className); |
| 107 | } |
| 108 | return Util.readBytes(classByteStream); |
| 109 | } catch (IOException e) { |
| 110 | throw new ClassNotFoundException( |
| 111 | "Couldn't load " + className + " from system class loader", e); |
| 112 | } |
| 113 | } |
| 114 | return super.getByteCode(className); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Classes like com.package.ClassName$InnerClass should also be loaded from the system class |
| 119 | * loader, so we test if the classes in the annotation are prefixes of the class to load. |
| 120 | */ |
| 121 | private boolean shouldLoadFromSystemLoader(String className) { |
| 122 | for (String classNamePrefix : mSystemLoaderClasses) { |
| 123 | if (className.startsWith(classNamePrefix)) { |
| 124 | return true; |
| 125 | } |
| 126 | } |
| 127 | return false; |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | private static class FrameworkSandboxFactory extends SandboxFactory { |
| 132 | private final Set<String> mSystemLoaderClasses; |
| 133 | |
| 134 | private FrameworkSandboxFactory(Set<String> systemLoaderClasses) { |
| 135 | mSystemLoaderClasses = systemLoaderClasses; |
| 136 | } |
| 137 | |
| 138 | @Nonnull |
| 139 | @Override |
| 140 | public ClassLoader createClassLoader( |
| 141 | InstrumentationConfiguration instrumentationConfig, URL... urls) { |
| 142 | return new FrameworkClassLoader( |
| 143 | mSystemLoaderClasses, |
| 144 | ClassLoader.getSystemClassLoader(), |
| 145 | instrumentationConfig, |
| 146 | urls); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | private static Set<String> classesToClassNames(Class<?>[] classes) { |
| 151 | ImmutableSet.Builder<String> builder = ImmutableSet.builder(); |
| 152 | for (Class<?> classObject : classes) { |
| 153 | builder.add(classObject.getName()); |
| 154 | } |
| 155 | return builder.build(); |
| 156 | } |
| 157 | } |