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.File; 31 import java.io.FileInputStream; 32 import java.io.FileOutputStream; 33 import java.io.InputStream; 34 import java.io.OutputStream; 35 import java.nio.file.Files; 36 import java.time.Duration; 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.UUID; 40 import java.util.regex.Pattern; 41 import java.util.zip.ZipEntry; 42 import java.util.zip.ZipOutputStream; 43 44 public class Utils { 45 private static final Duration SOFT_REBOOT_TIMEOUT = Duration.ofMinutes(3); 46 private static final Duration HOST_COMMAND_TIMEOUT = Duration.ofSeconds(10); 47 48 private final TestInformation mTestInfo; 49 Utils(TestInformation testInfo)50 public Utils(TestInformation testInfo) throws Exception { 51 assertThat(testInfo.getDevice()).isNotNull(); 52 mTestInfo = testInfo; 53 } 54 assertCommandSucceeds(String... command)55 public String assertCommandSucceeds(String... command) throws Exception { 56 CommandResult result = 57 mTestInfo.getDevice().executeShellV2Command(String.join(" ", command)); 58 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 59 // Remove trailing \n's. 60 return result.getStdout().trim(); 61 } 62 assertHostCommandSucceeds(String... command)63 public String assertHostCommandSucceeds(String... command) throws Exception { 64 CommandResult result = 65 RunUtil.getDefault().runTimedCmd(HOST_COMMAND_TIMEOUT.toMillis(), command); 66 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 67 // Remove trailing \n's. 68 return result.getStdout().trim(); 69 } 70 71 /** 72 * Implementation details. 73 * 74 * @param packages A list of packages, where each entry is a list of files in the package. 75 * @param multiPackage True for {@code install-multi-package}, false for {@code 76 * install-multiple}. 77 */ installImpl(IAbi abi, List<String> args, List<List<String>> packages, boolean multiPackage)78 private void installImpl(IAbi abi, List<String> args, List<List<String>> packages, 79 boolean multiPackage) throws Exception { 80 // We cannot use `ITestDevice.installPackage` or `SuiteApkInstaller` here because they don't 81 // support DM files. 82 List<String> cmd = 83 new ArrayList<>(List.of("adb", "-s", mTestInfo.getDevice().getSerialNumber(), 84 multiPackage ? "install-multi-package" : "install-multiple", "--abi", 85 abi.getName())); 86 87 cmd.addAll(args); 88 89 if (!multiPackage && packages.size() != 1) { 90 throw new IllegalArgumentException( 91 "'install-multiple' only supports exactly one package"); 92 } 93 94 for (List<String> files : packages) { 95 if (multiPackage) { 96 // The format is 'pkg1-base.dm:pkg1-base.apk:pkg1-split1.dm:pkg1-split1.apk 97 // pkg2-base.dm:pkg2-base.apk:pkg2-split1.dm:pkg2-split1.apk'. 98 cmd.add(String.join(":", files)); 99 } else { 100 // The format is 'pkg1-base.dm pkg1-base.apk pkg1-split1.dm pkg1-split1.apk'. 101 cmd.addAll(files); 102 } 103 } 104 105 // We can't use `INativeDevice.executeAdbCommand`. It only returns stdout on success and 106 // returns null on failure, while we want to get the exact error message. 107 CommandResult result = RunUtil.getDefault().runTimedCmd( 108 mTestInfo.getDevice().getOptions().getAdbCommandTimeout(), 109 cmd.toArray(String[] ::new)); 110 assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0); 111 } 112 113 /** 114 * Implementation details. 115 * 116 * @param packages A list of packages, where each entry is a list of APK-DM pairs. 117 * @param multiPackage True for {@code install-multi-package}, false for {@code 118 * install-multiple}. 119 */ installFromResourcesImpl(IAbi abi, List<String> args, List<List<Pair<String, String>>> packages, boolean multiPackage)120 private void installFromResourcesImpl(IAbi abi, List<String> args, 121 List<List<Pair<String, String>>> packages, boolean multiPackage) throws Exception { 122 List<List<String>> packageFileLists = new ArrayList<>(); 123 for (List<Pair<String, String>> apkDmResources : packages) { 124 List<String> files = new ArrayList<>(); 125 for (Pair<String, String> pair : apkDmResources) { 126 String apkResource = pair.first; 127 File apkFile = copyResourceToFile(apkResource, File.createTempFile("temp", ".apk")); 128 apkFile.deleteOnExit(); 129 130 String dmResource = pair.second; 131 if (dmResource != null) { 132 File dmFile = copyResourceToFile( 133 dmResource, new File(getDmPath(apkFile.getAbsolutePath()))); 134 dmFile.deleteOnExit(); 135 files.add(dmFile.getAbsolutePath()); 136 } 137 138 // To make `install-multi-package` happy, the last file must end with ".apk". 139 files.add(apkFile.getAbsolutePath()); 140 } 141 packageFileLists.add(files); 142 } 143 144 installImpl(abi, args, packageFileLists, multiPackage); 145 } 146 147 /** 148 * Installs a package from resources with arguments. 149 * 150 * @param apkDmResources For each pair, the first item is the APK resource name, and the second 151 * item is the DM resource name or null. 152 */ installFromResourcesWithArgs(IAbi abi, List<String> args, List<Pair<String, String>> apkDmResources)153 public void installFromResourcesWithArgs(IAbi abi, List<String> args, 154 List<Pair<String, String>> apkDmResources) throws Exception { 155 installFromResourcesImpl(abi, args, List.of(apkDmResources), false /* multiPackage */); 156 } 157 158 /** Same as above, but takes no argument. */ installFromResources(IAbi abi, List<Pair<String, String>> apkDmResources)159 public void installFromResources(IAbi abi, List<Pair<String, String>> apkDmResources) 160 throws Exception { 161 installFromResourcesWithArgs(abi, List.of() /* args */, apkDmResources); 162 } 163 installFromResources(IAbi abi, String apkResource, String dmResource)164 public void installFromResources(IAbi abi, String apkResource, String dmResource) 165 throws Exception { 166 installFromResources(abi, List.of(Pair.create(apkResource, dmResource))); 167 } 168 installFromResources(IAbi abi, String apkResource)169 public void installFromResources(IAbi abi, String apkResource) throws Exception { 170 installFromResources(abi, apkResource, null); 171 } 172 installFromResourcesMultiPackage( IAbi abi, List<List<Pair<String, String>>> packages)173 public void installFromResourcesMultiPackage( 174 IAbi abi, List<List<Pair<String, String>>> packages) throws Exception { 175 installFromResourcesImpl(abi, List.of() /* args */, packages, true /* multiPackage */); 176 } 177 installFromResourcesWithSdm(IAbi abi, String apkResource, File dmFile, File sdmFile)178 public void installFromResourcesWithSdm(IAbi abi, String apkResource, File dmFile, File sdmFile) 179 throws Exception { 180 File apkFile = copyResourceToFile(apkResource, File.createTempFile("temp", ".apk")); 181 apkFile.deleteOnExit(); 182 File dmFileCopy = new File(getDmPath(apkFile.getAbsolutePath())); 183 Files.copy(dmFile.toPath(), dmFileCopy.toPath()); 184 dmFileCopy.deleteOnExit(); 185 File sdmFileCopy = new File(getSdmPath(apkFile.getAbsolutePath())); 186 Files.copy(sdmFile.toPath(), sdmFileCopy.toPath()); 187 sdmFileCopy.deleteOnExit(); 188 189 installImpl(abi, List.of() /* args */, 190 List.of(List.of(dmFileCopy.getAbsolutePath(), sdmFileCopy.getAbsolutePath(), 191 apkFile.getAbsolutePath())), 192 true /* multiPackage */); 193 } 194 pushFromResource(String resource, String remotePath)195 public void pushFromResource(String resource, String remotePath) throws Exception { 196 File tempFile = copyResourceToFile(resource, File.createTempFile("temp", ".tmp")); 197 tempFile.deleteOnExit(); 198 assertThat(mTestInfo.getDevice().pushFile(tempFile, remotePath)).isTrue(); 199 } 200 copyResourceToFile(String resourceName, File file)201 public File copyResourceToFile(String resourceName, File file) throws Exception { 202 try (OutputStream outputStream = new FileOutputStream(file); 203 InputStream inputStream = getClass().getResourceAsStream(resourceName)) { 204 assertThat(ByteStreams.copy(inputStream, outputStream)).isGreaterThan(0); 205 } 206 return file; 207 } 208 softReboot()209 public void softReboot() throws Exception { 210 // `waitForBootComplete` relies on `dev.bootcomplete`. 211 mTestInfo.getDevice().executeShellCommand("setprop dev.bootcomplete 0"); 212 mTestInfo.getDevice().executeShellCommand("setprop ctl.restart zygote"); 213 boolean success = mTestInfo.getDevice().waitForBootComplete(SOFT_REBOOT_TIMEOUT.toMillis()); 214 assertWithMessage("Soft reboot didn't complete in %ss", SOFT_REBOOT_TIMEOUT.getSeconds()) 215 .that(success) 216 .isTrue(); 217 } 218 dumpContainsDexFile(String dump, String dexFile)219 public static void dumpContainsDexFile(String dump, String dexFile) { 220 assertThat(dump).containsMatch(dexFileToPattern(dexFile)); 221 } 222 dumpDoesNotContainDexFile(String dump, String dexFile)223 public static void dumpDoesNotContainDexFile(String dump, String dexFile) { 224 assertThat(dump).doesNotContainMatch(dexFileToPattern(dexFile)); 225 } 226 countSubstringOccurrence(String str, String subStr)227 public static int countSubstringOccurrence(String str, String subStr) { 228 return str.split(subStr, -1 /* limit */).length - 1; 229 } 230 generateCompilationArtifacts( String apkResource, String profileResource)231 public CompilationArtifacts generateCompilationArtifacts( 232 String apkResource, String profileResource) throws Exception { 233 String tempDir = "/data/local/tmp/CtsCompilationTestCases_" + UUID.randomUUID(); 234 assertCommandSucceeds("mkdir", tempDir); 235 String remoteApkFile = tempDir + "/app.apk"; 236 pushFromResource(apkResource, remoteApkFile); 237 String remoteProfileFile = tempDir + "/app.prof"; 238 pushFromResource(profileResource, remoteProfileFile); 239 240 String remoteOdexFile = tempDir + "/app.odex"; 241 String remoteVdexFile = tempDir + "/app.vdex"; 242 String remoteArtFile = tempDir + "/app.art"; 243 assertCommandSucceeds("dex2oat", "--dex-file=" + remoteApkFile, 244 "--profile-file=" + remoteProfileFile, "--oat-file=" + remoteOdexFile, 245 "--output-vdex=" + remoteVdexFile, "--app-image-file=" + remoteArtFile, 246 "--compiler-filter=speed-profile", "--compilation-reason=cloud"); 247 248 File odexFile = File.createTempFile("temp", ".odex"); 249 odexFile.deleteOnExit(); 250 assertThat(mTestInfo.getDevice().pullFile(remoteOdexFile, odexFile)).isTrue(); 251 File vdexFile = File.createTempFile("temp", ".vdex"); 252 vdexFile.deleteOnExit(); 253 assertThat(mTestInfo.getDevice().pullFile(remoteVdexFile, vdexFile)).isTrue(); 254 File artFile = File.createTempFile("temp", ".art"); 255 artFile.deleteOnExit(); 256 assertThat(mTestInfo.getDevice().pullFile(remoteArtFile, artFile)).isTrue(); 257 258 mTestInfo.getDevice().deleteFile(tempDir); 259 260 return new CompilationArtifacts(odexFile, vdexFile, artFile); 261 } 262 createDm(String profileResource, File vdexFile)263 public File createDm(String profileResource, File vdexFile) throws Exception { 264 File dmFile = File.createTempFile("test", ".dm"); 265 dmFile.deleteOnExit(); 266 try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dmFile))) { 267 zip.setLevel(0); 268 zip.putNextEntry(new ZipEntry("primary.prof")); 269 try (InputStream inputStream = getClass().getResourceAsStream(profileResource)) { 270 assertThat(ByteStreams.copy(inputStream, zip)).isGreaterThan(0); 271 } 272 zip.putNextEntry(new ZipEntry("primary.vdex")); 273 try (InputStream inputStream = new FileInputStream(vdexFile)) { 274 assertThat(ByteStreams.copy(inputStream, zip)).isGreaterThan(0); 275 } 276 zip.closeEntry(); 277 } 278 return dmFile; 279 } 280 281 // We cannot generate an SDM file in the build system because the contents have to come from the 282 // device. createSdm(File odexFile, File artFile)283 public File createSdm(File odexFile, File artFile) throws Exception { 284 File sdmFile = File.createTempFile("test", ".sdm"); 285 sdmFile.deleteOnExit(); 286 try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(sdmFile))) { 287 zip.setLevel(0); 288 zip.putNextEntry(new ZipEntry("primary.odex")); 289 try (InputStream inputStream = new FileInputStream(odexFile)) { 290 assertThat(ByteStreams.copy(inputStream, zip)).isGreaterThan(0); 291 } 292 zip.closeEntry(); 293 zip.putNextEntry(new ZipEntry("primary.art")); 294 try (InputStream inputStream = new FileInputStream(artFile)) { 295 assertThat(ByteStreams.copy(inputStream, zip)).isGreaterThan(0); 296 } 297 zip.closeEntry(); 298 } 299 signApk(sdmFile); 300 return sdmFile; 301 } 302 signApk(File file)303 private void signApk(File file) throws Exception { 304 File apksigner = mTestInfo.getDependencyFile("apksigner.jar", false /* targetFirst */); 305 File key = mTestInfo.getDependencyFile("testkey.pk8", false /* targetFirst */); 306 File cert = mTestInfo.getDependencyFile("testkey.x509.pem", false /* targetFirst */); 307 assertHostCommandSucceeds("java", "-jar", apksigner.getAbsolutePath(), "sign", "--key", 308 key.getAbsolutePath(), "--cert", cert.getAbsolutePath(), "--min-sdk-version=35", 309 file.getAbsolutePath()); 310 } 311 getDmPath(String apkPath)312 private String getDmPath(String apkPath) throws Exception { 313 return apkPath.replaceAll("\\.apk$", ".dm"); 314 } 315 getSdmPath(String apkPath)316 private String getSdmPath(String apkPath) throws Exception { 317 return apkPath.replaceAll("\\.apk$", ".sdm"); 318 } 319 dexFileToPattern(String dexFile)320 private static Pattern dexFileToPattern(String dexFile) { 321 return Pattern.compile(String.format("[\\s/](%s)\\s?", Pattern.quote(dexFile))); 322 } 323 324 /** Represents the compilation artifacts of an APK. All the files are on host. */ CompilationArtifacts(File odexFile, File vdexFile, File artFile)325 public record CompilationArtifacts(File odexFile, File vdexFile, File artFile) {} 326 } 327