1 /* 2 * Copyright (C) 2018 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 package android.signature.cts.api; 17 18 import android.app.Instrumentation; 19 import android.os.Bundle; 20 import android.provider.Settings; 21 import android.signature.cts.ApiDocumentParser; 22 import android.signature.cts.ClassProvider; 23 import android.signature.cts.ExcludingClassProvider; 24 import android.signature.cts.ExpectedFailuresFilter; 25 import android.signature.cts.FailureType; 26 import android.signature.cts.JDiffClassDescription; 27 import android.signature.cts.ResultObserver; 28 import android.signature.cts.VirtualPath; 29 import android.util.Log; 30 import androidx.test.platform.app.InstrumentationRegistry; 31 import androidx.test.runner.AndroidJUnit4; 32 import com.android.compatibility.common.util.DynamicConfigDeviceSide; 33 import com.google.common.base.Suppliers; 34 35 import java.util.ArrayList; 36 import java.util.Collection; 37 import java.util.Collections; 38 import java.util.function.Predicate; 39 import java.util.function.Supplier; 40 import java.util.stream.Stream; 41 import org.junit.AfterClass; 42 import org.junit.Before; 43 import org.junit.runner.RunWith; 44 45 import static org.junit.Assert.assertEquals; 46 import static org.junit.Assert.assertNull; 47 48 /** 49 * Base class for the signature tests. 50 */ 51 @RunWith(AndroidJUnit4.class) 52 public abstract class AbstractApiTest { 53 54 private static final String TAG = "AbstractApiTest"; 55 56 /** 57 * The name of the optional instrumentation option that contains the name of the dynamic config 58 * data set that contains the expected failures. 59 */ 60 private static final String DYNAMIC_CONFIG_NAME_OPTION = "dynamic-config-name"; 61 62 private TestResultObserver mResultObserver; 63 64 ClassProvider mClassProvider; 65 66 private static Predicate<String> sListFilteringPredicate = null; 67 setListFilteringPredicate(Predicate<String> p)68 public static void setListFilteringPredicate(Predicate<String> p) { 69 sListFilteringPredicate = p; 70 } 71 72 /** 73 * The list of expected failures. 74 */ 75 private Collection<String> expectedFailures = Collections.emptyList(); 76 77 @AfterClass closeResourceStore()78 public static void closeResourceStore() { 79 ResourceStore.close(); 80 } 81 getInstrumentation()82 public Instrumentation getInstrumentation() { 83 return InstrumentationRegistry.getInstrumentation(); 84 } 85 getGlobalExemptions()86 protected String getGlobalExemptions() { 87 return Settings.Global.getString( 88 getInstrumentation().getContext().getContentResolver(), 89 Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS); 90 } 91 getGlobalHiddenApiPolicy()92 protected String getGlobalHiddenApiPolicy() { 93 return Settings.Global.getString( 94 getInstrumentation().getContext().getContentResolver(), 95 Settings.Global.HIDDEN_API_POLICY); 96 } 97 98 @Before setUp()99 public void setUp() throws Exception { 100 mResultObserver = new TestResultObserver(); 101 102 // Get the arguments passed to the instrumentation. 103 Bundle instrumentationArgs = InstrumentationRegistry.getArguments(); 104 105 // Check that the device is in the correct state for running this test. 106 assertEquals( 107 String.format("Device in bad state: %s is not as expected", 108 Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS), 109 getExpectedBlocklistExemptions(), 110 getGlobalExemptions()); 111 assertNull( 112 String.format("Device in bad state: %s is not as expected", 113 Settings.Global.HIDDEN_API_POLICY), 114 getGlobalHiddenApiPolicy()); 115 116 117 // Prepare for a class provider that loads classes from bootclasspath but filters 118 // out known inaccessible classes. 119 // Note that com.android.internal.R.* inner classes are also excluded as they are 120 // not part of API though exist in the runtime. 121 mClassProvider = new ExcludingClassProvider( 122 new BootClassPathClassesProvider(), 123 name -> name != null && name.startsWith("com.android.internal.R.")); 124 125 String dynamicConfigName = instrumentationArgs.getString(DYNAMIC_CONFIG_NAME_OPTION); 126 if (dynamicConfigName != null) { 127 // Get the DynamicConfig.xml contents and extract the expected failures list. 128 DynamicConfigDeviceSide dcds = new DynamicConfigDeviceSide(dynamicConfigName); 129 Collection<String> expectedFailures = dcds.getValues("expected_failures"); 130 initExpectedFailures(expectedFailures); 131 } 132 133 initializeFromArgs(instrumentationArgs); 134 } 135 136 /** 137 * Initialize the expected failures. 138 * 139 * <p>Call from with {@link #setUp()}</p> 140 * 141 * @param expectedFailures the expected failures. 142 */ initExpectedFailures(Collection<String> expectedFailures)143 private void initExpectedFailures(Collection<String> expectedFailures) { 144 this.expectedFailures = expectedFailures; 145 String tag = getClass().getName(); 146 Log.d(tag, "Expected failure count: " + expectedFailures.size()); 147 for (String failure: expectedFailures) { 148 Log.d(tag, "Expected failure: \"" + failure + "\""); 149 } 150 } 151 getExpectedBlocklistExemptions()152 protected String getExpectedBlocklistExemptions() { 153 return null; 154 } 155 initializeFromArgs(Bundle instrumentationArgs)156 protected void initializeFromArgs(Bundle instrumentationArgs) throws Exception { 157 } 158 159 protected interface RunnableWithResultObserver { run(ResultObserver observer)160 void run(ResultObserver observer) throws Exception; 161 } 162 runWithTestResultObserver(RunnableWithResultObserver runnable)163 void runWithTestResultObserver(RunnableWithResultObserver runnable) { 164 runWithTestResultObserver(expectedFailures, runnable); 165 } 166 runWithTestResultObserver( Collection<String> expectedFailures, RunnableWithResultObserver runnable)167 private void runWithTestResultObserver( 168 Collection<String> expectedFailures, RunnableWithResultObserver runnable) { 169 try { 170 ResultObserver observer = mResultObserver; 171 if (!expectedFailures.isEmpty()) { 172 observer = new ExpectedFailuresFilter(observer, expectedFailures); 173 } 174 runnable.run(observer); 175 } catch (Error|Exception e) { 176 mResultObserver.notifyFailure( 177 FailureType.CAUGHT_EXCEPTION, 178 e.getClass().getName(), 179 "Uncaught exception thrown by test", 180 e); 181 } 182 mResultObserver.onTestComplete(); // Will throw is there are failures 183 } 184 getSupplierOfAnOptionalCommaSeparatedListArgument(String key)185 static Supplier<String[]> getSupplierOfAnOptionalCommaSeparatedListArgument(String key) { 186 return Suppliers.memoize(() -> { 187 Bundle arguments = InstrumentationRegistry.getArguments(); 188 return getCommaSeparatedListOptional(arguments, key); 189 })::get; 190 } 191 maybeFilterCommaSeparatedElements(String elements)192 static String[] maybeFilterCommaSeparatedElements(String elements) { 193 // default implementation is unfiltered 194 String[] allElements = elements.split(","); 195 if (sListFilteringPredicate == null) { 196 return allElements; 197 } 198 final ArrayList<String> filteredElements = new ArrayList<>(); 199 for (String s : allElements) { 200 if (sListFilteringPredicate.test(s)) { 201 Log.d(TAG, "maybeFilterCommaSeparatedElements adding filtered element: " + s); 202 filteredElements.add(s); 203 } 204 } 205 return filteredElements.toArray(new String[filteredElements.size()]); 206 } 207 getCommaSeparatedListOptional(Bundle instrumentationArgs, String key)208 static String[] getCommaSeparatedListOptional(Bundle instrumentationArgs, String key) { 209 String argument = instrumentationArgs.getString(key); 210 if (argument == null) { 211 return new String[0]; 212 } 213 return maybeFilterCommaSeparatedElements(argument); 214 } 215 getSupplierOfAMandatoryCommaSeparatedListArgument(String key)216 static Supplier<String[]> getSupplierOfAMandatoryCommaSeparatedListArgument(String key) { 217 return Suppliers.memoize(() -> { 218 Bundle arguments = InstrumentationRegistry.getArguments(); 219 return getCommaSeparatedListRequired(arguments, key); 220 })::get; 221 } 222 223 static String[] getCommaSeparatedListRequired(Bundle instrumentationArgs, String key) { 224 String argument = instrumentationArgs.getString(key); 225 if (argument == null) { 226 throw new IllegalStateException("Could not find required argument '" + key + "'"); 227 } 228 return maybeFilterCommaSeparatedElements(argument); 229 } 230 231 /** 232 * Create a stream of {@link JDiffClassDescription} by parsing a set of API resource files. 233 * 234 * @param apiDocumentParser the parser to use. 235 * @param apiResources the list of API resource files. 236 * 237 * @return the stream of {@link JDiffClassDescription}. 238 */ 239 Stream<JDiffClassDescription> parseApiResourcesAsStream( 240 ApiDocumentParser apiDocumentParser, String[] apiResources) { 241 return retrieveApiResourcesAsStream(getClass().getClassLoader(), apiResources) 242 .flatMap(apiDocumentParser::parseAsStream); 243 } 244 245 /** 246 * Retrieve a stream of {@link VirtualPath} from a list of API resource files. 247 * 248 * <p>Any zip files are flattened, i.e. if a resource name ends with {@code .zip} then it is 249 * unpacked into a temporary directory and the paths to the unpacked files are returned instead 250 * of the path to the zip file.</p> 251 * 252 * @param classLoader the {@link ClassLoader} from which the resources will be loaded. 253 * @param apiResources the list of API resource files. 254 * 255 * @return the stream of {@link VirtualPath}. 256 */ 257 static Stream<VirtualPath> retrieveApiResourcesAsStream( 258 ClassLoader classLoader, 259 String[] apiResources) { 260 return Stream.of(apiResources) 261 .flatMap(resourceName -> ResourceStore.readResource(classLoader, resourceName)); 262 } 263 } 264