1 /*
2  * Copyright (C) 2011 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 android.signature.cts.api;
18 
19 import android.app.Instrumentation;
20 import android.content.Context;
21 import android.content.pm.SharedLibraryInfo;
22 import android.signature.cts.ApiComplianceChecker;
23 import android.signature.cts.ApiDocumentParser;
24 import android.signature.cts.JDiffClassDescription;
25 import android.signature.cts.VirtualPath;
26 import android.util.Log;
27 import androidx.test.platform.app.InstrumentationRegistry;
28 import com.google.common.base.Suppliers;
29 import java.io.BufferedReader;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.InputStreamReader;
33 import java.util.List;
34 import java.util.Set;
35 import java.util.TreeSet;
36 import java.util.function.Supplier;
37 import java.util.stream.Collectors;
38 import java.util.stream.Stream;
39 import junitparams.JUnitParamsRunner;
40 import junitparams.Parameters;
41 import junitparams.naming.TestCaseName;
42 import org.junit.Assume;
43 import org.junit.FixMethodOrder;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 import org.junit.runners.MethodSorters;
47 
48 /**
49  * Verifies that any shared library provided by this device and for which this test has a
50  * corresponding API specific file provides the expected API.
51  *
52  * <pre>This test relies on the AndroidManifest.xml file for the APK in which this is run having a
53  * {@code <uses-library>} entry for every shared library that provides an API that is contained
54  * within the shared-libs-all.api.zip supplied to this test.
55  */
56 @RunWith(JUnitParamsRunner.class)
57 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
58 public class SignatureMultiLibsTest extends AbstractSignatureTest {
59 
60     protected static final Supplier<String[]> EXPECTED_API_FILES =
61             getSupplierOfAMandatoryCommaSeparatedListArgument(EXPECTED_API_FILES_ARG);
62 
63     protected static final Supplier<String[]> PREVIOUS_API_FILES =
64             getSupplierOfAMandatoryCommaSeparatedListArgument(PREVIOUS_API_FILES_ARG);
65 
66     private static final String TAG = SignatureMultiLibsTest.class.getSimpleName();
67 
68     /**
69      * A memoized supplier of the list of shared libraries on the device.
70      */
71     protected static final Supplier<Set<String>> AVAILABLE_SHARED_LIBRARIES =
72             Suppliers.memoize(SignatureMultiLibsTest::retrieveActiveSharedLibraries)::get;
73 
74     private static final String SHARED_LIBRARY_LIST_RESOURCE_NAME = "shared-libs-names.txt";
75 
76     /**
77      * Retrieve the names of the shared libraries that are active on the device.
78      *
79      * @return The set of shared library names.
80      */
retrieveActiveSharedLibraries()81     private static Set<String> retrieveActiveSharedLibraries() {
82         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
83         Context context = instrumentation.getTargetContext();
84 
85         List<SharedLibraryInfo> sharedLibraries =
86                 context.getPackageManager().getSharedLibraries(0);
87 
88         Set<String> sharedLibraryNames = new TreeSet<>();
89         for (SharedLibraryInfo sharedLibrary : sharedLibraries) {
90             String name = sharedLibrary.getName();
91             sharedLibraryNames.add(name);
92             Log.d(TAG, String.format("Found library: %s%n", name));
93         }
94 
95         return sharedLibraryNames;
96     }
97 
98     /**
99      * A memoized supplier of the list of shared libraries that this can test.
100      */
101     protected static final Supplier<List<String>> TESTABLE_SHARED_LIBRARIES =
102             Suppliers.memoize(SignatureMultiLibsTest::retrieveTestableSharedLibraries)::get;
103 
104     /**
105      * Retrieve the names of the shared libraries that are testable by this test.
106      *
107      * @return The set of shared library names.
108      */
retrieveTestableSharedLibraries()109     private static List<String> retrieveTestableSharedLibraries() {
110         ClassLoader classLoader = SignatureMultiLibsTest.class.getClassLoader();
111         try (InputStream is = classLoader.getResourceAsStream(SHARED_LIBRARY_LIST_RESOURCE_NAME)) {
112             if (is == null) {
113                 throw new RuntimeException(
114                         "Resource " + SHARED_LIBRARY_LIST_RESOURCE_NAME + " could not be found");
115             }
116 
117             try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
118                 return reader.lines()
119                         .filter(line -> !line.isEmpty())
120                         .sorted()
121                         .collect(Collectors.toList());
122             }
123         } catch (IOException e) {
124             throw new RuntimeException("Could not retrieve testable shared libraries", e);
125         }
126     }
127 
128     /**
129      * Convert the list of testable shared libraries into a form suitable for parameterizing a test.
130      */
getTestableSharedLibraryParameters()131     public Object[][] getTestableSharedLibraryParameters() {
132         List<String> libraries = TESTABLE_SHARED_LIBRARIES.get();
133         Object[][] params = new Object[libraries.size()][1];
134         for (int i = 0; i < libraries.size(); i++) {
135             String name = libraries.get(i);
136             TestableLibraryParameter param = new TestableLibraryParameter(name);
137             params[i][0] = param;
138         }
139         return params;
140     }
141 
142     /**
143      * Skips the test if the supplied library is unavailable on the device.
144      *
145      * <p>If the library is unavailable then this throws an
146      * {@link org.junit.AssumptionViolatedException}.</p>
147      *
148      * @param library the name of the library that needs to be available.
149      */
skipTestIfLibraryIsUnavailable(String library)150     private void skipTestIfLibraryIsUnavailable(String library) {
151         Assume.assumeTrue("Shared library " + library + " is not available on this device",
152                 AVAILABLE_SHARED_LIBRARIES.get().contains(library));
153     }
154 
155     /**
156      * Return a stream of {@link JDiffClassDescription} that are expected to be provided by the
157      * shared libraries which are installed on this device.
158      *
159      * @param apiDocumentParser the parser to use.
160      * @param apiResources the list of API resource files.
161      * @param library the name of the library whose APIs should be parsed.
162      * @return a stream of {@link JDiffClassDescription}.
163      */
parseActiveSharedLibraryApis( ApiDocumentParser apiDocumentParser, String[] apiResources, String library)164     private Stream<JDiffClassDescription> parseActiveSharedLibraryApis(
165             ApiDocumentParser apiDocumentParser, String[] apiResources, String library) {
166 
167         return retrieveApiResourcesAsStream(getClass().getClassLoader(), apiResources)
168                 .filter(path -> {
169                     String apiLibraryName = getLibraryNameFromPath(path);
170                     return apiLibraryName.equals(library);
171                 })
172                 .flatMap(apiDocumentParser::parseAsStream);
173     }
174 
175     /**
176      * Tests that each shared library's API matches its current API.
177      *
178      * <p>One test per shared library, checks the entire API, and then reports the complete list of
179      * failures.</p>
180      */
181     @Test
182     // Parameterize this method with the set of testable shared libraries.
183     @Parameters(method = "getTestableSharedLibraryParameters")
184     // The test name is the method name followed by and _ and the shared library name, with .s
185     // replaced with _. e.g. testRuntimeCompatibilityWithCurrentApi_android_test_base.
186     @TestCaseName("{method}_{0}")
testRuntimeCompatibilityWithCurrentApi(TestableLibraryParameter parameter)187     public void testRuntimeCompatibilityWithCurrentApi(TestableLibraryParameter parameter) {
188         String library = parameter.getName();
189         skipTestIfLibraryIsUnavailable(library);
190         runWithTestResultObserver(mResultObserver -> {
191             ApiComplianceChecker complianceChecker =
192                     new ApiComplianceChecker(mResultObserver, mClassProvider);
193 
194             // Load classes from any API files that form the base which the expected APIs extend.
195             loadBaseClasses(complianceChecker);
196 
197             ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
198 
199             parseActiveSharedLibraryApis(apiDocumentParser, EXPECTED_API_FILES.get(), library)
200                     .forEach(complianceChecker::checkSignatureCompliance);
201 
202             // After done parsing all expected API files, perform any deferred checks.
203             complianceChecker.checkDeferred();
204         });
205     }
206 
207     /**
208      * Tests that each shared library's API matches its previous APIs.
209      *
210      * <p>One test per shared library, checks the entire API, and then reports the complete list of
211      * failures.</p>
212      */
213     @Test
214     // Parameterize this method with the set of testable shared libraries.
215     @Parameters(method = "getTestableSharedLibraryParameters")
216     // The test name is the method name followed by and _ and the shared library name, with .s
217     // replaced with _. e.g. testRuntimeCompatibilityWithPreviousApis_android_test_base.
218     @TestCaseName("{method}_{0}")
testRuntimeCompatibilityWithPreviousApis(TestableLibraryParameter parameter)219     public void testRuntimeCompatibilityWithPreviousApis(TestableLibraryParameter parameter) {
220         String library = parameter.getName();
221         skipTestIfLibraryIsUnavailable(library);
222         runWithTestResultObserver(mResultObserver -> {
223             ApiComplianceChecker complianceChecker =
224                     new ApiComplianceChecker(mResultObserver, mClassProvider);
225 
226             ApiDocumentParser apiDocumentParser = new ApiDocumentParser(TAG);
227 
228             parseActiveSharedLibraryApis(apiDocumentParser, PREVIOUS_API_FILES.get(), library)
229                     .map(clazz -> clazz.setPreviousApiFlag(true))
230                     .forEach(complianceChecker::checkSignatureCompliance);
231 
232             // After done parsing all expected API files, perform any deferred checks.
233             complianceChecker.checkDeferred();
234         });
235     }
236 
237     /**
238      * Get the library name from the API file's path.
239      *
240      * @param path the path of the API file.
241      * @return the library name for the API file.
242      */
getLibraryNameFromPath(VirtualPath path)243     private String getLibraryNameFromPath(VirtualPath path) {
244         String name = path.toString();
245         return name.substring(name.lastIndexOf('/') + 1).split("-")[0];
246     }
247 
248     /**
249      * A wrapper around a shared library name to ensure that its string representation is suitable
250      * for use in a parameterized test name, i.e. does not contain any characters that are not
251      * allowed in a test name by CTS/AndroidJUnitRunner.
252      */
253     public static class TestableLibraryParameter {
254         private final String name;
255         private final String parameter;
256 
TestableLibraryParameter(String name)257         public TestableLibraryParameter(String name) {
258             this.name = name;
259             this.parameter = name.replace('.', '_');
260         }
261 
getName()262         public String getName() {
263             return name;
264         }
265 
266         @Override
toString()267         public String toString() {
268             return parameter;
269         }
270     }
271 }
272