1 /*
2  * Copyright (C) 2021 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.os.classpath;
18 
19 import static android.compat.testing.Classpaths.ClasspathType.BOOTCLASSPATH;
20 import static android.compat.testing.Classpaths.ClasspathType.DEX2OATBOOTCLASSPATH;
21 import static android.compat.testing.Classpaths.ClasspathType.SYSTEMSERVERCLASSPATH;
22 import static android.compat.testing.Classpaths.getJarsOnClasspath;
23 
24 import static com.android.os.classpath.ClasspathsTest.ClasspathSubject.assertThat;
25 
26 import static com.google.common.base.Preconditions.checkArgument;
27 import static com.google.common.truth.Fact.fact;
28 import static com.google.common.truth.Fact.simpleFact;
29 import static com.google.common.truth.Truth.assertAbout;
30 import static com.google.common.truth.Truth.assertWithMessage;
31 
32 import static org.junit.Assume.assumeTrue;
33 
34 import com.android.modules.utils.build.testing.DeviceSdkLevel;
35 import com.android.tradefed.device.ITestDevice;
36 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
37 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
38 
39 import com.google.common.collect.ImmutableList;
40 import com.google.common.truth.Fact;
41 import com.google.common.truth.FailureMetadata;
42 import com.google.common.truth.IterableSubject;
43 import com.google.common.truth.Ordered;
44 
45 import org.junit.Before;
46 import org.junit.Test;
47 import org.junit.runner.RunWith;
48 
49 import java.nio.file.Paths;
50 
51 /** Tests for the contents of *CLASSPATH environ variables on a device. */
52 @RunWith(DeviceJUnit4ClassRunner.class)
53 public class ClasspathsTest extends BaseHostJUnit4Test {
54 
55     // A selection of jars on *CLASSPATH that cover all categories:
56     // - ART apex jar
57     // - Non-updatable apex jar
58     // - Updatable apex jar
59     // - System jar on BOOTCLASSPATH
60     // - System jar on SYSTEMSERVERCLASSPATH
61     private static final String FRAMEWORK_JAR = "/system/framework/framework.jar";
62     private static final String ICU4J_JAR = "/apex/com.android.i18n/javalib/core-icu4j.jar";
63     private static final String LIBART_JAR = "/apex/com.android.art/javalib/core-libart.jar";
64     private static final String SDKEXTENSIONS_JAR =
65             "/apex/com.android.sdkext/javalib/framework-sdkextensions.jar";
66     private static final String SERVICES_JAR = "/system/framework/services.jar";
67 
68     @Before
before()69     public void before() throws Exception {
70         ITestDevice device = getDevice();
71         DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(device);
72         assumeTrue(deviceSdkLevel.isDeviceAtLeastS());
73     }
74 
75     @Test
testBootclasspath()76     public void testBootclasspath() throws Exception {
77         ImmutableList<String> jars = getJarsOnClasspath(getDevice(), BOOTCLASSPATH);
78 
79         assertThat(jars).containsNoDuplicates();
80 
81         assertThat(jars)
82                 .containsAtLeast(LIBART_JAR, FRAMEWORK_JAR, ICU4J_JAR, SDKEXTENSIONS_JAR)
83                 .inOrder();
84         assertThat(jars).doesNotContain(SERVICES_JAR);
85 
86         ImmutableList<String> expectedPrefixes =
87                 ImmutableList.of(
88                         "/apex/com.android.art/",
89                         "/system/",
90                         "/system_ext/",
91                         "/apex/com.android.i18n/",
92                         "/apex/");
93         assertThat(jars).prefixesMatch(expectedPrefixes).inOrder();
94 
95         assertThat(getUpdatableApexes(jars)).isInOrder();
96     }
97 
98     @Test
testDex2oatBootclasspath()99     public void testDex2oatBootclasspath() throws Exception {
100         ImmutableList<String> jars = getJarsOnClasspath(getDevice(), DEX2OATBOOTCLASSPATH);
101 
102         assertThat(jars).containsNoDuplicates();
103 
104         // DEX2OATBOOTCLASSPATH must only contain ART, core-icu4j, and platform system jars
105         assertThat(jars).containsAtLeast(LIBART_JAR, FRAMEWORK_JAR, ICU4J_JAR).inOrder();
106         assertThat(jars).containsNoneOf(SDKEXTENSIONS_JAR, SERVICES_JAR);
107 
108         // DEX2OATBOOTCLASSPATH must be a subset of BOOTCLASSPATH
109         ImmutableList<String> bootJars = getJarsOnClasspath(getDevice(), BOOTCLASSPATH);
110         assertThat(bootJars).containsAtLeastElementsIn(jars);
111 
112         ImmutableList<String> expectedPrefixes =
113                 ImmutableList.of(
114                         "/apex/com.android.art/",
115                         "/system/",
116                         "/system_ext/",
117                         "/apex/com.android.i18n/");
118         assertThat(jars).prefixesMatch(expectedPrefixes).inOrder();
119 
120         // No updatable jars on DEX2OATBOOTCLASSPATH
121         assertThat(getUpdatableApexes(jars)).isEmpty();
122     }
123 
124     @Test
testSystemServerClasspath()125     public void testSystemServerClasspath() throws Exception {
126         ImmutableList<String> jars = getJarsOnClasspath(getDevice(), SYSTEMSERVERCLASSPATH);
127 
128         assertThat(jars).containsNoDuplicates();
129 
130         assertThat(jars).containsNoneOf(LIBART_JAR, FRAMEWORK_JAR, ICU4J_JAR, SDKEXTENSIONS_JAR);
131         assertThat(jars).contains(SERVICES_JAR);
132 
133         ImmutableList<String> expectedPrefixes =
134                 ImmutableList.of("/system/", "/system_ext/", "/apex/");
135         assertThat(jars).prefixesMatch(expectedPrefixes).inOrder();
136 
137         assertThat(getUpdatableApexes(jars)).isInOrder();
138     }
139 
140     @Test
testDex2oatJarsAreFirstOnBootclasspath()141     public void testDex2oatJarsAreFirstOnBootclasspath() throws Exception {
142         ImmutableList<String> bootJars = getJarsOnClasspath(getDevice(), BOOTCLASSPATH);
143         ImmutableList<String> dex2oatJars = getJarsOnClasspath(getDevice(), DEX2OATBOOTCLASSPATH);
144 
145         // All preopt jars on BOOTCLASSPATH must come before updatable jars.
146         assertThat(bootJars).startsWith(dex2oatJars);
147     }
148 
149     /**
150      * Returns a derived subject with names of the updatable APEXes preserving the original order.
151      */
getUpdatableApexes(ImmutableList<String> jars)152     private static ImmutableList<String> getUpdatableApexes(ImmutableList<String> jars) {
153         return jars.stream()
154                 .filter(jar -> jar.startsWith("/apex"))
155                 // ICU4J_JAR is the last non-updatable APEX jar, i.e. everything after is
156                 // considered to be an updatable APEX jar
157                 .dropWhile(jar -> !jar.equals(ICU4J_JAR))
158                 .skip(1)
159                 // Map to APEX name from "/apex/<name>/javalibs/foo.jar"
160                 .map(jar -> Paths.get(jar).getName(1).toString())
161                 .collect(ImmutableList.toImmutableList());
162     }
163 
164     static final class ClasspathSubject extends IterableSubject {
165 
166         private static final Ordered EMPTY_ORDERED = () -> {};
167 
168         private final ImmutableList<String> actual;
169 
ClasspathSubject(FailureMetadata metadata, ImmutableList<String> iterable)170         protected ClasspathSubject(FailureMetadata metadata, ImmutableList<String> iterable) {
171             super(metadata, iterable);
172             actual = iterable;
173         }
174 
assertThat(ImmutableList<String> jars)175         public static ClasspathSubject assertThat(ImmutableList<String> jars) {
176             return assertAbout(ClasspathSubject::new).that(jars);
177         }
178 
179         /**
180          * Checks that the actual iterable contains only jars that start with the expected prefixes
181          * or fails.
182          *
183          * <p>To also test that the prefixes appear in the given order, make a call to {@code
184          * inOrder} on the object returned by this method. The expected elements must appear in the
185          * given order within the actual elements.
186          */
prefixesMatch(ImmutableList<String> expected)187         public Ordered prefixesMatch(ImmutableList<String> expected) {
188             checkArgument(
189                     expected.stream().distinct().count() == expected.size(),
190                     "No duplicates are allowed in expected values.");
191 
192             ImmutableList.Builder<String> unexpectedJars = ImmutableList.builder();
193             boolean ordered = true;
194             int currentPrefixIndex = expected.isEmpty() ? -1 : 0;
195             for (String jar : actual) {
196                 int prefixIndex = findFirstMatchingPrefix(jar, expected);
197                 if (prefixIndex == -1) {
198                     unexpectedJars.add(jar);
199                     continue;
200                 }
201                 if (prefixIndex != currentPrefixIndex) {
202                     if (prefixIndex < currentPrefixIndex) {
203                         ordered = false;
204                     }
205                     currentPrefixIndex = prefixIndex;
206                 }
207             }
208 
209             ImmutableList<String> unexpected = unexpectedJars.build();
210             if (!unexpected.isEmpty()) {
211                 Fact expectedOrder =
212                         fact("expected jar filepaths to be prefixes with one of", expected);
213                 ImmutableList.Builder<Fact> facts = ImmutableList.builder();
214                 for (String e : unexpected) {
215                     facts.add(fact("unexpected", e));
216                 }
217                 facts.add(simpleFact("---"));
218                 facts.add(simpleFact(""));
219                 failWithoutActual(expectedOrder, facts.build().toArray(new Fact[0]));
220                 return EMPTY_ORDERED;
221             }
222 
223             if (ordered) {
224                 return EMPTY_ORDERED;
225             }
226 
227             return () ->
228                     failWithActual(
229                             simpleFact("all jars have valid partitions, but the order was wrong"),
230                             fact("expected order", expected));
231         }
232 
233         /** Checks that the actual iterable starts with expected elements. */
startsWith(ImmutableList<String> expected)234         public void startsWith(ImmutableList<String> expected) {
235             if (actual.size() < expected.size()) {
236                 failWithActual("expected at least number of elements", expected.size());
237                 return;
238             }
239             assertWithMessage("Unexpected initial elements of the list")
240                     .that(actual.subList(0, expected.size()))
241                     .isEqualTo(expected);
242         }
243 
findFirstMatchingPrefix(String value, ImmutableList<String> prefixes)244         private static int findFirstMatchingPrefix(String value, ImmutableList<String> prefixes) {
245             for (int i = 0; i < prefixes.size(); i++) {
246                 if (value.startsWith(prefixes.get(i))) {
247                     return i;
248                 }
249             }
250             return -1;
251         }
252     }
253 }
254