1 /* 2 * Copyright (C) 2023 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.compilation.cts; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import com.android.tradefed.invoker.TestInformation; 23 import com.android.tradefed.testtype.IAbi; 24 import com.android.tradefed.util.CommandResult; 25 import com.android.tradefed.util.Pair; 26 import com.android.tradefed.util.RunUtil; 27 28 import com.google.common.io.ByteStreams; 29 30 import java.io.BufferedInputStream; 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.OutputStream; 37 import java.nio.file.Files; 38 import java.time.Duration; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Objects; 43 import java.util.UUID; 44 import java.util.regex.Pattern; 45 import java.util.zip.CRC32; 46 import java.util.zip.ZipEntry; 47 import java.util.zip.ZipOutputStream; 48 49 public class Utils { 50 private static final Duration SOFT_REBOOT_TIMEOUT = Duration.ofMinutes(3); 51 private static final Duration HOST_COMMAND_TIMEOUT = Duration.ofSeconds(10); 52 53 // Keep in sync with `ABI_TO_INSTRUCTION_SET_MAP` in 54 // libcore/libart/src/main/java/dalvik/system/VMRuntime.java. 55 // clang-format off 56 private static final Map<String, String> ABI_TO_INSTRUCTION_SET_MAP = Map.of( 57 "armeabi", "arm", 58 "armeabi-v7a", "arm", 59 "x86", "x86", 60 "x86_64", "x86_64", 61 "arm64-v8a", "arm64", 62 "arm64-v8a-hwasan", "arm64", 63 "riscv64", "riscv64" 64 ); 65 // clang-format on 66 67 private final TestInformation mTestInfo; 68 Utils(TestInformation testInfo)69 public Utils(TestInformation testInfo) throws Exception { 70 assertThat(testInfo.getDevice()).isNotNull(); 71 mTestInfo = testInfo; 72 } 73 assertCommandSucceeds(String... command)74 public String assertCommandSucceeds(String... command) throws Exception { 75 CommandResult result = 76 mTestInfo.getDevice().executeShellV2Command(String.join(" ", command)); 77 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 78 // Remove trailing \n's. 79 return result.getStdout().trim(); 80 } 81 assertHostCommandSucceeds(String... command)82 public String assertHostCommandSucceeds(String... command) throws Exception { 83 CommandResult result = 84 RunUtil.getDefault().runTimedCmd(HOST_COMMAND_TIMEOUT.toMillis(), command); 85 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 86 // Remove trailing \n's. 87 return result.getStdout().trim(); 88 } 89 90 /** 91 * Implementation details. 92 * 93 * @param packages A list of packages, where each entry is a list of files in the package. 94 * @param multiPackage True for {@code install-multi-package}, false for {@code 95 * install-multiple}. 96 */ installImpl(IAbi abi, List<String> args, List<List<String>> packages, boolean multiPackage)97 private void installImpl(IAbi abi, List<String> args, List<List<String>> packages, 98 boolean multiPackage) throws Exception { 99 // We cannot use `ITestDevice.installPackage` or `SuiteApkInstaller` here because they don't 100 // support DM files. 101 List<String> cmd = 102 new ArrayList<>(List.of("adb", "-s", mTestInfo.getDevice().getSerialNumber(), 103 multiPackage ? "install-multi-package" : "install-multiple", "--abi", 104 abi.getName())); 105 106 cmd.addAll(args); 107 108 if (!multiPackage && packages.size() != 1) { 109 throw new IllegalArgumentException( 110 "'install-multiple' only supports exactly one package"); 111 } 112 113 for (List<String> files : packages) { 114 if (multiPackage) { 115 // The format is 'pkg1-base.dm:pkg1-base.apk:pkg1-split1.dm:pkg1-split1.apk 116 // pkg2-base.dm:pkg2-base.apk:pkg2-split1.dm:pkg2-split1.apk'. 117 cmd.add(String.join(":", files)); 118 } else { 119 // The format is 'pkg1-base.dm pkg1-base.apk pkg1-split1.dm pkg1-split1.apk'. 120 cmd.addAll(files); 121 } 122 } 123 124 // We can't use `INativeDevice.executeAdbCommand`. It only returns stdout on success and 125 // returns null on failure, while we want to get the exact error message. 126 CommandResult result = RunUtil.getDefault().runTimedCmd( 127 mTestInfo.getDevice().getOptions().getAdbCommandTimeout(), 128 cmd.toArray(String[] ::new)); 129 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 130 } 131 132 /** 133 * Implementation details. 134 * 135 * @param packages A list of packages, where each entry is a list of APK-DM pairs. 136 * @param multiPackage True for {@code install-multi-package}, false for {@code 137 * install-multiple}. 138 */ installFromResourcesImpl(IAbi abi, List<String> args, List<List<Pair<String, String>>> packages, boolean multiPackage)139 private void installFromResourcesImpl(IAbi abi, List<String> args, 140 List<List<Pair<String, String>>> packages, boolean multiPackage) throws Exception { 141 List<List<String>> packageFileLists = new ArrayList<>(); 142 for (List<Pair<String, String>> apkDmResources : packages) { 143 List<String> files = new ArrayList<>(); 144 for (Pair<String, String> pair : apkDmResources) { 145 String apkResource = pair.first; 146 File apkFile = copyResourceToFile(apkResource, File.createTempFile("temp", ".apk")); 147 apkFile.deleteOnExit(); 148 149 String dmResource = pair.second; 150 if (dmResource != null) { 151 File dmFile = copyResourceToFile( 152 dmResource, new File(getDmPath(apkFile.getAbsolutePath()))); 153 dmFile.deleteOnExit(); 154 files.add(dmFile.getAbsolutePath()); 155 } 156 157 // To make `install-multi-package` happy, the last file must end with ".apk". 158 files.add(apkFile.getAbsolutePath()); 159 } 160 packageFileLists.add(files); 161 } 162 163 installImpl(abi, args, packageFileLists, multiPackage); 164 } 165 166 /** 167 * Installs a package from resources with arguments. 168 * 169 * @param apkDmResources For each pair, the first item is the APK resource name, and the second 170 * item is the DM resource name or null. 171 */ installFromResourcesWithArgs(IAbi abi, List<String> args, List<Pair<String, String>> apkDmResources)172 public void installFromResourcesWithArgs(IAbi abi, List<String> args, 173 List<Pair<String, String>> apkDmResources) throws Exception { 174 installFromResourcesImpl(abi, args, List.of(apkDmResources), false /* multiPackage */); 175 } 176 177 /** Same as above, but takes no argument. */ installFromResources(IAbi abi, List<Pair<String, String>> apkDmResources)178 public void installFromResources(IAbi abi, List<Pair<String, String>> apkDmResources) 179 throws Exception { 180 installFromResourcesWithArgs(abi, List.of() /* args */, apkDmResources); 181 } 182 installFromResources(IAbi abi, String apkResource, String dmResource)183 public void installFromResources(IAbi abi, String apkResource, String dmResource) 184 throws Exception { 185 installFromResources(abi, List.of(Pair.create(apkResource, dmResource))); 186 } 187 installFromResources(IAbi abi, String apkResource)188 public void installFromResources(IAbi abi, String apkResource) throws Exception { 189 installFromResources(abi, apkResource, null); 190 } 191 installFromResourcesMultiPackage( IAbi abi, List<List<Pair<String, String>>> packages)192 public void installFromResourcesMultiPackage( 193 IAbi abi, List<List<Pair<String, String>>> packages) throws Exception { 194 installFromResourcesImpl(abi, List.of() /* args */, packages, true /* multiPackage */); 195 } 196 installFromResourcesWithSdm(IAbi abi, String apkResource, File dmFile, File sdmFile)197 public void installFromResourcesWithSdm(IAbi abi, String apkResource, File dmFile, File sdmFile) 198 throws Exception { 199 File apkFile = copyResourceToFile(apkResource, File.createTempFile("temp", ".apk")); 200 apkFile.deleteOnExit(); 201 File dmFileCopy = new File(getDmPath(apkFile.getAbsolutePath())); 202 Files.copy(dmFile.toPath(), dmFileCopy.toPath()); 203 dmFileCopy.deleteOnExit(); 204 File sdmFileCopy = new File(getSdmPath(apkFile.getAbsolutePath(), abi)); 205 Files.copy(sdmFile.toPath(), sdmFileCopy.toPath()); 206 sdmFileCopy.deleteOnExit(); 207 208 installImpl(abi, List.of() /* args */, 209 List.of(List.of(dmFileCopy.getAbsolutePath(), sdmFileCopy.getAbsolutePath(), 210 apkFile.getAbsolutePath())), 211 true /* multiPackage */); 212 } 213 pushFromResource(String resource, String remotePath)214 public void pushFromResource(String resource, String remotePath) throws Exception { 215 File tempFile = copyResourceToFile(resource, File.createTempFile("temp", ".tmp")); 216 tempFile.deleteOnExit(); 217 assertThat(mTestInfo.getDevice().pushFile(tempFile, remotePath)).isTrue(); 218 } 219 copyResourceToFile(String resourceName, File file)220 public File copyResourceToFile(String resourceName, File file) throws Exception { 221 try (OutputStream outputStream = new FileOutputStream(file); 222 InputStream inputStream = getClass().getResourceAsStream(resourceName)) { 223 assertThat(ByteStreams.copy(inputStream, outputStream)).isGreaterThan(0); 224 } 225 return file; 226 } 227 softReboot()228 public void softReboot() throws Exception { 229 // `waitForBootComplete` relies on `dev.bootcomplete`. 230 mTestInfo.getDevice().executeShellCommand("setprop dev.bootcomplete 0"); 231 mTestInfo.getDevice().executeShellCommand("setprop ctl.restart zygote"); 232 boolean success = mTestInfo.getDevice().waitForBootComplete(SOFT_REBOOT_TIMEOUT.toMillis()); 233 assertWithMessage("Soft reboot didn't complete in %ss", SOFT_REBOOT_TIMEOUT.getSeconds()) 234 .that(success) 235 .isTrue(); 236 } 237 dumpContainsDexFile(String dump, String dexFile)238 public static void dumpContainsDexFile(String dump, String dexFile) { 239 assertThat(dump).containsMatch(dexFileToPattern(dexFile)); 240 } 241 dumpDoesNotContainDexFile(String dump, String dexFile)242 public static void dumpDoesNotContainDexFile(String dump, String dexFile) { 243 assertThat(dump).doesNotContainMatch(dexFileToPattern(dexFile)); 244 } 245 countSubstringOccurrence(String str, String subStr)246 public static int countSubstringOccurrence(String str, String subStr) { 247 return str.split(subStr, -1 /* limit */).length - 1; 248 } 249 generateCompilationArtifacts(String apkResource, String profileResource, IAbi abi, String classLoaderContext)250 public CompilationArtifacts generateCompilationArtifacts(String apkResource, 251 String profileResource, IAbi abi, String classLoaderContext) throws Exception { 252 String tempDir = "/data/local/tmp/CtsCompilationTestCases_" + UUID.randomUUID(); 253 assertCommandSucceeds("mkdir", tempDir); 254 String remoteApkFile = tempDir + "/app.apk"; 255 pushFromResource(apkResource, remoteApkFile); 256 String remoteProfileFile = tempDir + "/app.prof"; 257 pushFromResource(profileResource, remoteProfileFile); 258 259 String remoteOdexFile = tempDir + "/app.odex"; 260 String remoteVdexFile = tempDir + "/app.vdex"; 261 String remoteArtFile = tempDir + "/app.art"; 262 String isa = getTranslatedIsa( 263 Objects.requireNonNull(ABI_TO_INSTRUCTION_SET_MAP.get(abi.getName()))); 264 assertCommandSucceeds( 265 "dex2oat", 266 "--instruction-set=" + isa, 267 "--dex-file=" + remoteApkFile, 268 "--dex-location=base.apk", 269 "--profile-file=" + remoteProfileFile, 270 "--oat-file=" + remoteOdexFile, 271 "--output-vdex=" + remoteVdexFile, 272 "--app-image-file=" + remoteArtFile, 273 "--compiler-filter=speed-profile", 274 "--compilation-reason=cloud", 275 "--class-loader-context=" + classLoaderContext); 276 277 File odexFile = File.createTempFile("temp", ".odex"); 278 odexFile.deleteOnExit(); 279 assertThat(mTestInfo.getDevice().pullFile(remoteOdexFile, odexFile)).isTrue(); 280 File vdexFile = File.createTempFile("temp", ".vdex"); 281 vdexFile.deleteOnExit(); 282 assertThat(mTestInfo.getDevice().pullFile(remoteVdexFile, vdexFile)).isTrue(); 283 File artFile = File.createTempFile("temp", ".art"); 284 artFile.deleteOnExit(); 285 assertThat(mTestInfo.getDevice().pullFile(remoteArtFile, artFile)).isTrue(); 286 287 mTestInfo.getDevice().deleteFile(tempDir); 288 289 return new CompilationArtifacts(odexFile, vdexFile, artFile); 290 } 291 createDm(String profileResource, File vdexFile)292 public File createDm(String profileResource, File vdexFile) throws Exception { 293 File dmFile = File.createTempFile("test", ".dm"); 294 dmFile.deleteOnExit(); 295 try (ZipWriter zipWriter = new ZipWriter(dmFile)) { 296 zipWriter.addUncompressedAlignedEntry( 297 "primary.prof", getClass().getResourceAsStream(profileResource)); 298 zipWriter.addUncompressedAlignedEntry("primary.vdex", new FileInputStream(vdexFile)); 299 } 300 return dmFile; 301 } 302 303 // We cannot generate an SDM file in the build system because the contents have to come from the 304 // device. createSdm(File odexFile, File artFile, String keyName)305 public File createSdm(File odexFile, File artFile, String keyName) throws Exception { 306 File sdmFile = File.createTempFile("test", ".sdm"); 307 sdmFile.deleteOnExit(); 308 try (ZipWriter zipWriter = new ZipWriter(sdmFile)) { 309 zipWriter.addUncompressedAlignedEntry("primary.odex", new FileInputStream(odexFile)); 310 zipWriter.addUncompressedAlignedEntry("primary.art", new FileInputStream(artFile)); 311 } 312 if (keyName != null) { 313 signApk(sdmFile, keyName); 314 } 315 return sdmFile; 316 } 317 signApk(File file, String keyName)318 private void signApk(File file, String keyName) throws Exception { 319 File apksigner = mTestInfo.getDependencyFile("apksigner.jar", false /* targetFirst */); 320 File key = mTestInfo.getDependencyFile(keyName + ".pk8", false /* targetFirst */); 321 File cert = mTestInfo.getDependencyFile(keyName + ".x509.pem", false /* targetFirst */); 322 assertHostCommandSucceeds("java", "-jar", apksigner.getAbsolutePath(), "sign", "--key", 323 key.getAbsolutePath(), "--cert", cert.getAbsolutePath(), "--min-sdk-version=35", 324 "--alignment-preserved", file.getAbsolutePath()); 325 } 326 getDmPath(String apkPath)327 private String getDmPath(String apkPath) throws Exception { 328 return apkPath.replaceAll("\\.apk$", ".dm"); 329 } 330 getSdmPath(String apkPath, IAbi abi)331 private String getSdmPath(String apkPath, IAbi abi) throws Exception { 332 String isa = getTranslatedIsa( 333 Objects.requireNonNull(ABI_TO_INSTRUCTION_SET_MAP.get(abi.getName()))); 334 return apkPath.replaceAll("\\.apk$", "." + isa + ".sdm"); 335 } 336 dexFileToPattern(String dexFile)337 private static Pattern dexFileToPattern(String dexFile) { 338 return Pattern.compile(String.format("[\\s/](%s)\\s?", Pattern.quote(dexFile))); 339 } 340 341 /** 342 * If the given ISA is not native to the device, and the native bridge exists, returns the ISA 343 * that the native bridge translates it to. Otherwise, returns the ISA as is. 344 */ getTranslatedIsa(String isa)345 private String getTranslatedIsa(String isa) throws Exception { 346 String translatedIsa = mTestInfo.getDevice().getProperty("ro.dalvik.vm.isa." + isa); 347 return translatedIsa != null ? translatedIsa : isa; 348 } 349 350 /** A {@link ZipOutputStream} wrapper that helps create uncompressed aligned entries. */ 351 public static class ZipWriter implements AutoCloseable { 352 /** The length of the local file header, in bytes, excluding variable length fields. */ 353 private static final int LOCAL_FILE_HEADER_EXCL_VER_FIELDS_LEN = 30; 354 /** 355 * The zip entry alignment, in bytes. 356 * 357 * Actually, we don't need to align this much. Only odex needs to align to page size, as 358 * required by the Bionic's dlopen, while other files only need to align to 4 bytes, as 359 * required by ART. We align all to 16KB just for simplicity. 360 */ 361 private static final int ALIGNMENT = 16384; 362 363 private final ZipOutputStream mZip; 364 private long mOffset = 0; 365 ZipWriter(File zipFile)366 public ZipWriter(File zipFile) throws IOException { 367 mZip = new ZipOutputStream(new FileOutputStream(zipFile)); 368 } 369 370 @Override close()371 public void close() throws IOException { 372 mZip.close(); 373 } 374 375 /** Add an uncompressed aligned entry. */ addUncompressedAlignedEntry(String name, InputStream stream)376 public void addUncompressedAlignedEntry(String name, InputStream stream) 377 throws IOException { 378 mZip.setMethod(ZipOutputStream.STORED); 379 try (InputStream inputStream = new BufferedInputStream(stream)) { 380 inputStream.mark(Integer.MAX_VALUE); 381 382 // We have to calculate CRC32 and the size ourselves because `ZipOutputStream` 383 // doesn't do it for STORED entries. 384 CRC32 crc = new CRC32(); 385 long size = 0; 386 byte[] buf = new byte[8192]; 387 int n; 388 while ((n = inputStream.read(buf)) != -1) { 389 crc.update(buf, 0, n); 390 size += n; 391 } 392 393 inputStream.reset(); 394 395 // The zip file structure looks like: 396 // +------------------------------------+---------+---------+ 397 // | Entry 1 | Entry 2 | ... | 398 // +-----------------------------+------+---------+---------+ 399 // | Local file header | | | | 400 // +----------+----------+-------+ Data | ... | ... | 401 // | 30 bytes | Filename | Extra | | | | 402 // +----------+----------+-------+------+---------+---------+ 403 // We put null padding as extra, to achieve alignment. 404 mOffset += LOCAL_FILE_HEADER_EXCL_VER_FIELDS_LEN + name.length(); 405 int padding = 406 (mOffset % ALIGNMENT > 0) ? (ALIGNMENT - (int) (mOffset % ALIGNMENT)) : 0; 407 mOffset += padding; 408 409 ZipEntry zipEntry = new ZipEntry(name); 410 zipEntry.setSize(size); 411 zipEntry.setCompressedSize(size); 412 zipEntry.setCrc(crc.getValue()); 413 zipEntry.setExtra(new byte[padding]); 414 mZip.putNextEntry(zipEntry); 415 416 assertThat(ByteStreams.copy(inputStream, mZip)).isGreaterThan(0); 417 mOffset += size; 418 419 mZip.closeEntry(); 420 } 421 } 422 } 423 424 /** Represents the compilation artifacts of an APK. All the files are on host. */ CompilationArtifacts(File odexFile, File vdexFile, File artFile)425 public record CompilationArtifacts(File odexFile, File vdexFile, File artFile) {} 426 } 427