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.targetprep; 18 19 import static android.compat.testing.Classpaths.ClasspathType.BOOTCLASSPATH; 20 import static android.compat.testing.Classpaths.ClasspathType.SYSTEMSERVERCLASSPATH; 21 22 import android.compat.testing.Classpaths; 23 import android.compat.testing.Classpaths.ClasspathType; 24 25 import com.android.modules.proto.ClasspathClasses.Classpath; 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 30 import com.android.tradefed.config.Option; 31 import com.android.tradefed.device.DeviceNotAvailableException; 32 import com.android.tradefed.device.INativeDevice; 33 import com.android.tradefed.device.ITestDevice; 34 import com.android.tradefed.invoker.TestInformation; 35 import com.android.tradefed.log.LogUtil.CLog; 36 import com.android.tradefed.targetprep.BaseTargetPreparer; 37 import com.android.tradefed.targetprep.TargetSetupError; 38 import com.android.tradefed.util.RunUtil; 39 import com.google.common.collect.ImmutableList; 40 import com.google.common.collect.ImmutableSet; 41 42 import java.io.File; 43 import java.io.FileOutputStream; 44 import java.io.IOException; 45 import java.nio.file.Files; 46 import java.nio.file.Path; 47 import java.util.Objects; 48 49 import com.android.tools.smali.dexlib2.iface.ClassDef; 50 51 /* 52 * Target preparer that fetches classpath relevant artifacts for a test in a 'reentrant' manner. 53 * 54 * <p>The preparer will fetch all <b>BOOTCLASSPATH</b>, <b>SYSTEMSERVERCLASSPATH</b> and shared 55 * libraries from the device, parse their contents and dump the classnames into a temporary 56 * directory.</p> 57 * 58 * <p>Additionally, the preparer <i>can</i> fetch, parse and dump data for module specific artifacts 59 * (i.e. apk-in-apex) if specified.</p> 60 * 61 * <p>Nested runs of the artifact fetcher (i.e. in the case of a top-level test config xml and a 62 * child test xml) will only fetch non-common elements, and remove temporary class dumps fetched 63 * during that particular preparer run.</p> 64 * 65 * <p>Each module's conformance framework test config xml must run the preparer (with module 66 * specific parameters) before the entrypoint test jar, and the top-level conformance framework 67 * config xml must also run it before any other module xml.</p> 68 * 69 * <p>The upshot is that when running all conformance framework tests for all modules, the shared 70 * artifacts are fetched and processed only once.</p> 71 */ 72 public class ClasspathFetcher extends BaseTargetPreparer { 73 74 public static final String DEVICE_JAR_ARTIFACTS_TAG = "device-jar-artifacts"; 75 public static final String BCP_CLASSES_FILE = "bcp.pb"; 76 public static final String SSCP_CLASSES_FILE = "sscp.pb"; 77 78 // TODO(andreionea): also fetch classes for standalone system server jars, apk-in-apex and 79 // shared libraries. They require more mocking on the test side. 80 81 public static final String APEX_PKG_TAG = "apex-package"; 82 // Special case for fetching only non-updatable platform. 83 public static final String PLATFORM_PACKAGE = "platform"; 84 85 @Option(name = "apex-package", 86 description = "The package name of the apex under test.") 87 private String mApexPackage; 88 89 private boolean mFetchedArtifacts = false; 90 91 @Override setUp(TestInformation testInfo)92 public void setUp(TestInformation testInfo) 93 throws TargetSetupError, DeviceNotAvailableException { 94 Objects.requireNonNull(testInfo.getDevice()); 95 if (mApexPackage != null) { 96 testInfo.properties().put(APEX_PKG_TAG, mApexPackage); 97 } 98 // The artifacts have been fetched already, no need to do anything else. 99 if (testInfo.properties().containsKey(DEVICE_JAR_ARTIFACTS_TAG)) { 100 return; 101 } 102 try { 103 final Path tmpDir = Files.createTempDirectory("device_artifacts"); 104 testInfo.properties().put(DEVICE_JAR_ARTIFACTS_TAG, 105 tmpDir.toAbsolutePath().toString()); 106 107 getClassesInClasspath(testInfo.getDevice(), BOOTCLASSPATH) 108 .writeTo(new FileOutputStream(new File(tmpDir.toFile(), BCP_CLASSES_FILE))); 109 getClassesInClasspath(testInfo.getDevice(), SYSTEMSERVERCLASSPATH) 110 .writeTo(new FileOutputStream(new File(tmpDir.toFile(), SSCP_CLASSES_FILE))); 111 112 mFetchedArtifacts = true; 113 } catch(IOException e) { 114 throw new RuntimeException("Could not create temp artifacts dir!", e); 115 } 116 } 117 118 @Override tearDown(TestInformation testInfo, Throwable e)119 public void tearDown(TestInformation testInfo, Throwable e) { 120 if (mFetchedArtifacts) { 121 try { 122 final String path = testInfo.properties().get(DEVICE_JAR_ARTIFACTS_TAG); 123 if (path == null) { 124 throw new IllegalStateException("Target preparer has previously fetched" 125 + " artifacts, but the DEVICE_JAR_ARTIFACTS_TAG property was removed"); 126 } 127 final File jarArtifactsDir = new File(path); 128 deleteDirectory(jarArtifactsDir); 129 } finally { 130 testInfo.properties().remove(DEVICE_JAR_ARTIFACTS_TAG); 131 } 132 } 133 } 134 classpathTypeToClasspathEnum(ClasspathType t)135 private Classpath classpathTypeToClasspathEnum(ClasspathType t) { 136 switch(t) { 137 case BOOTCLASSPATH: 138 return Classpath.valueOf(Classpath.BOOTCLASSPATH_VALUE); 139 case SYSTEMSERVERCLASSPATH: 140 return Classpath.valueOf(Classpath.SYSTEMSERVERCLASSPATH_VALUE); 141 default: 142 throw new RuntimeException("Unknown classpath type " + t); 143 } 144 } 145 getClassesInFile(INativeDevice device, String file)146 private ImmutableSet<String> getClassesInFile(INativeDevice device, String file) 147 throws DeviceNotAvailableException, IOException { 148 final File jar = device.pullFile(file); 149 if (jar == null) { 150 throw new IllegalStateException("could not pull remote file " + file); 151 } 152 return Classpaths.getClassDefsFromJar(jar) 153 .stream() 154 .map(ClassDef::getType) 155 .collect(ImmutableSet.toImmutableSet()); 156 } 157 getClassesInClasspath(INativeDevice device, ClasspathType type)158 private ClasspathClassesDump getClassesInClasspath(INativeDevice device, ClasspathType type) 159 throws DeviceNotAvailableException, IOException { 160 ClasspathClassesDump.Builder builder = ClasspathClassesDump.newBuilder(); 161 final ImmutableList<String> jars = Classpaths.getJarsOnClasspath(device, type); 162 for (String jar : jars) { 163 ClasspathEntry.Builder entryBuilder = ClasspathEntry.newBuilder(); 164 Jar.Builder jarBuilder = Jar.newBuilder(); 165 jarBuilder.setClasspath(classpathTypeToClasspathEnum(type)); 166 jarBuilder.setPath(jar); 167 entryBuilder.setJar(jarBuilder.build()); 168 entryBuilder.addAllClasses(getClassesInFile(device, jar)); 169 builder.addEntries(entryBuilder.build()); 170 } 171 return builder.build(); 172 } 173 174 175 /** 176 * Deletes a directory and its contents recursively 177 * 178 * @param directory to delete 179 */ deleteDirectory(File directory)180 private static void deleteDirectory(File directory) { 181 File[] files = directory.listFiles(); 182 if (files != null) { 183 for (File file : files) { 184 if (!file.isDirectory()) { 185 file.delete(); 186 } else { 187 deleteDirectory(file); 188 } 189 } 190 } 191 directory.delete(); 192 } 193 194 }