xref: /aosp_15_r20/cts/hostsidetests/compilation/src/android/compilation/cts/Utils.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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