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