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