1 /*
2  * Copyright (C) 2022 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.modules.conformanceframework;
18 
19 
20 import static com.google.common.truth.Truth.assertThat;
21 import static com.google.common.truth.Truth.assertWithMessage;
22 
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assume.assumeTrue;
25 
26 import com.android.modules.proto.ClasspathClasses.ClasspathClassesDump;
27 import com.android.modules.proto.ClasspathClasses.ClasspathEntry;
28 import com.android.modules.proto.ClasspathClasses.Jar;
29 import com.android.modules.targetprep.ClasspathFetcher;
30 import com.android.modules.utils.build.testing.DeviceSdkLevel;
31 import com.android.tools.smali.dexlib2.iface.ClassDef;
32 import com.android.tradefed.config.Option;
33 import com.android.tradefed.device.DeviceNotAvailableException;
34 import com.android.tradefed.device.ITestDevice;
35 import com.android.tradefed.invoker.TestInformation;
36 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
37 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
38 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
39 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
40 
41 import com.google.common.collect.HashMultimap;
42 import com.google.common.collect.ImmutableCollection;
43 import com.google.common.collect.ImmutableList;
44 import com.google.common.collect.ImmutableMap;
45 import com.google.common.collect.ImmutableMultimap;
46 import com.google.common.collect.ImmutableSet;
47 import com.google.common.collect.ImmutableSetMultimap;
48 import com.google.common.collect.Multimap;
49 import com.google.common.collect.Multimaps;
50 
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 import java.io.File;
56 import java.io.FileInputStream;
57 import java.io.IOException;
58 import java.util.Arrays;
59 import java.util.Collection;
60 import java.util.Objects;
61 import java.util.Set;
62 import java.util.stream.Collectors;
63 import java.util.stream.Stream;
64 
65 
66 
67 /**
68  * Tests for detecting no duplicate class files are present on BOOTCLASSPATH and
69  * SYSTEMSERVERCLASSPATH.
70  *
71  * <p>Duplicate class files are not safe as some of the jars on *CLASSPATH are updated outside of
72  * the main dessert release cycle; they also contribute to unnecessary disk space usage.
73  */
74 @RunWith(DeviceJUnit4ClassRunner.class)
75 public class DuplicateClassesTest extends BaseHostJUnit4Test {
76     private static ImmutableSet<String> sBootclasspathJars;
77     private static ImmutableSet<String> sSystemserverclasspathJars;
78 
79     private static ImmutableMultimap<String, String> sJarsToClasses;
80     private static String sApexPackage;
81 
82     private DeviceSdkLevel mDeviceSdkLevel;
83 
84     /**
85      * Fetch all classpath info extracted by ClasspathFetcher.
86      *
87      */
88     @BeforeClassWithInfo
setupOnce(TestInformation testInfo)89     public static void setupOnce(TestInformation testInfo) throws Exception {
90         final String dctArtifactsPath = Objects.requireNonNull(
91                 testInfo.properties().get(ClasspathFetcher.DEVICE_JAR_ARTIFACTS_TAG));
92         sApexPackage = testInfo.properties().get(ClasspathFetcher.APEX_PKG_TAG);
93         final ImmutableMultimap.Builder<String, String> jarsToClasses =
94                 new ImmutableMultimap.Builder<>();
95         final File bcpDumpFile = new File(dctArtifactsPath, ClasspathFetcher.BCP_CLASSES_FILE);
96         final ClasspathClassesDump bcpDump =
97                 ClasspathClassesDump.parseFrom(new FileInputStream(bcpDumpFile));
98         sBootclasspathJars = bcpDump.getEntriesList().stream()
99             .map(entry -> entry.getJar().getPath())
100             .collect(ImmutableSet.toImmutableSet());
101         bcpDump.getEntriesList().stream()
102             .forEach(entry -> {
103                 jarsToClasses.putAll(entry.getJar().getPath(), entry.getClassesList());
104             });
105         final File sscpDumpFile = new File(dctArtifactsPath, ClasspathFetcher.SSCP_CLASSES_FILE);
106         final ClasspathClassesDump sscpDump =
107                 ClasspathClassesDump.parseFrom(new FileInputStream(sscpDumpFile));
108         sSystemserverclasspathJars = sscpDump.getEntriesList().stream()
109             .map(entry -> entry.getJar().getPath())
110             .collect(ImmutableSet.toImmutableSet());
111             sscpDump.getEntriesList().stream()
112             .forEach(entry -> {
113                 jarsToClasses.putAll(entry.getJar().getPath(), entry.getClassesList());
114             });
115         sJarsToClasses = jarsToClasses.build();
116     }
117 
118     @Before
setup()119     public void setup() {
120         mDeviceSdkLevel = new DeviceSdkLevel(getDevice());
121     }
122 
123     /**
124      * Ensure that there are no duplicate classes among jars listed in BOOTCLASSPATH.
125      */
126     @Test
testBootclasspath_nonDuplicateClasses()127     public void testBootclasspath_nonDuplicateClasses() throws Exception {
128         assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
129         assertThat(getDuplicateClasses(sBootclasspathJars)).isEmpty();
130     }
131 
132     /**
133      * Ensure that there are no duplicate classes among jars listed in SYSTEMSERVERCLASSPATH.
134      */
135     @Test
testSystemserverClasspath_nonDuplicateClasses()136     public void testSystemserverClasspath_nonDuplicateClasses() throws Exception {
137         assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
138         assertThat(getDuplicateClasses(sSystemserverclasspathJars)).isEmpty();
139     }
140 
141     /**
142      * Ensure that there are no duplicate classes among jars listed in BOOTCLASSPATH and
143      * SYSTEMSERVERCLASSPATH.
144      */
145     @Test
testSystemserverAndBootClasspath_nonDuplicateClasses()146     public void testSystemserverAndBootClasspath_nonDuplicateClasses() throws Exception {
147         assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
148         final ImmutableSet.Builder<String> jars = new ImmutableSet.Builder<>();
149         jars.addAll(sBootclasspathJars);
150         jars.addAll(sSystemserverclasspathJars);
151         assertThat(getDuplicateClasses(jars.build())).isEmpty();
152     }
153 
154     /**
155      * Gets the duplicate classes within a list of jar files.
156      *
157      * @param jars a list of jar files.
158      * @return a multimap with the class name as a key and the jar files as a value.
159      */
getDuplicateClasses(ImmutableCollection<String> jars)160     private Multimap<String, String> getDuplicateClasses(ImmutableCollection<String> jars) {
161         final HashMultimap<String, String> allClasses = HashMultimap.create();
162         Multimaps.invertFrom(Multimaps.filterKeys(sJarsToClasses, jars::contains), allClasses);
163         return Multimaps.filterKeys(allClasses, key -> validDuplicates(allClasses.get(key)));
164     }
165 
166     /**
167      * Filtering function for excluding invalid / uninteresting duplicates.
168      *
169      * This will filter out classes that are in only 1 jar, or duplicates that
170      * do not include jars in the apex under test.
171      */
172 
validDuplicates(Collection<String> duplicateJars)173     private boolean validDuplicates(Collection<String> duplicateJars) {
174         if (duplicateJars.size() <= 1) {
175             return false;
176         }
177         if (sApexPackage.equals(ClasspathFetcher.PLATFORM_PACKAGE)) {
178             return duplicateJars.stream()
179                 .anyMatch(jar -> !jar.startsWith("/apex"));
180         }
181         final String apexPrefix = "/apex/" + sApexPackage;
182         return duplicateJars.stream()
183             .anyMatch(jar -> jar.startsWith(apexPrefix));
184 
185     }
186 }
187