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