• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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