• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.tests.odsign;
18 
19 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
20 
21 import static com.google.common.truth.Truth.assertWithMessage;
22 
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNull;
25 import static org.junit.Assert.assertTrue;
26 import static org.junit.Assume.assumeTrue;
27 
28 import android.cts.install.lib.host.InstallUtilsHost;
29 
30 import com.android.tradefed.device.DeviceNotAvailableException;
31 import com.android.tradefed.device.ITestDevice;
32 import com.android.tradefed.device.ITestDevice.ApexInfo;
33 import com.android.tradefed.device.TestDeviceOptions;
34 import com.android.tradefed.invoker.TestInformation;
35 import com.android.tradefed.result.FileInputStreamSource;
36 import com.android.tradefed.result.LogDataType;
37 import com.android.tradefed.util.CommandResult;
38 
39 import java.io.File;
40 import java.time.Duration;
41 import java.time.ZonedDateTime;
42 import java.time.format.DateTimeFormatter;
43 import java.util.Arrays;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Optional;
47 import java.util.Set;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 import java.util.stream.Collectors;
51 import java.util.stream.Stream;
52 
53 public class OdsignTestUtils {
54     public static final String ART_APEX_DALVIK_CACHE_DIRNAME =
55             "/data/misc/apexdata/com.android.art/dalvik-cache";
56 
57     public static final List<String> ZYGOTE_NAMES = List.of("zygote", "zygote64");
58 
59     public static final List<String> APP_ARTIFACT_EXTENSIONS = List.of(".art", ".odex", ".vdex");
60     public static final List<String> BCP_ARTIFACT_EXTENSIONS = List.of(".art", ".oat", ".vdex");
61 
62     private static final String ODREFRESH_COMPILATION_LOG =
63             "/data/misc/odrefresh/compilation-log.txt";
64 
65     private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(5);
66     private static final Duration RESTART_ZYGOTE_COMPLETE_TIMEOUT = Duration.ofMinutes(3);
67 
68     private static final String TAG = "OdsignTestUtils";
69     private static final String PACKAGE_NAME_KEY = TAG + ":PACKAGE_NAME";
70 
71     private final InstallUtilsHost mInstallUtils;
72     private final TestInformation mTestInfo;
73 
OdsignTestUtils(TestInformation testInfo)74     public OdsignTestUtils(TestInformation testInfo) throws Exception {
75         assertNotNull(testInfo.getDevice());
76         mInstallUtils = new InstallUtilsHost(testInfo);
77         mTestInfo = testInfo;
78     }
79 
80     /**
81      * Re-installs the current active ART module on device.
82      */
installTestApex()83     public void installTestApex() throws Exception {
84         assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
85 
86         String packagesOutput =
87                 mTestInfo.getDevice().executeShellCommand("pm list packages -f --apex-only");
88         Pattern p = Pattern.compile(
89                 "^package:(.*)=(com(?:\\.google)?\\.android(?:\\.go)?\\.art)$",
90                 Pattern.MULTILINE);
91         Matcher m = p.matcher(packagesOutput);
92         assertTrue("ART module not found. Packages are:\n" + packagesOutput, m.find());
93         String artApexPath = m.group(1);
94         String artApexName = m.group(2);
95 
96         CommandResult result = mTestInfo.getDevice().executeShellV2Command(
97                 "pm install --apex " + artApexPath);
98         assertWithMessage("Failed to install APEX. Reason: " + result.toString())
99             .that(result.getExitCode()).isEqualTo(0);
100 
101         mTestInfo.properties().put(PACKAGE_NAME_KEY, artApexName);
102 
103         removeCompilationLogToAvoidBackoff();
104     }
105 
uninstallTestApex()106     public void uninstallTestApex() throws Exception {
107         String packageName = mTestInfo.properties().get(PACKAGE_NAME_KEY);
108         if (packageName != null) {
109             mTestInfo.getDevice().uninstallPackage(packageName);
110             removeCompilationLogToAvoidBackoff();
111         }
112     }
113 
getMappedArtifacts(String pid, String grepPattern)114     public Set<String> getMappedArtifacts(String pid, String grepPattern) throws Exception {
115         final String grepCommand = String.format("grep \"%s\" /proc/%s/maps", grepPattern, pid);
116         CommandResult result = mTestInfo.getDevice().executeShellV2Command(grepCommand);
117         assertTrue(result.toString(), result.getExitCode() == 0);
118         Set<String> mappedFiles = new HashSet<>();
119         for (String line : result.getStdout().split("\\R")) {
120             int start = line.indexOf(ART_APEX_DALVIK_CACHE_DIRNAME);
121             if (line.contains("[")) {
122                 continue; // ignore anonymously mapped sections which are quoted in square braces.
123             }
124             mappedFiles.add(line.substring(start));
125         }
126         return mappedFiles;
127     }
128 
129     /**
130      * Returns the mapped artifacts of the Zygote process, or {@code Optional.empty()} if the
131      * process does not exist.
132      */
getZygoteLoadedArtifacts(String zygoteName)133     public Optional<Set<String>> getZygoteLoadedArtifacts(String zygoteName) throws Exception {
134         final CommandResult result =
135                 mTestInfo.getDevice().executeShellV2Command("pidof " + zygoteName);
136         if (result.getExitCode() != 0) {
137             return Optional.empty();
138         }
139         // There may be multiple Zygote processes when Zygote just forks and has not executed any
140         // app binary. We can take any of the pids.
141         // We can't use the "-s" flag when calling `pidof` because the Toybox's `pidof`
142         // implementation is wrong and it outputs multiple pids regardless of the "-s" flag, so we
143         // split the output and take the first pid ourselves.
144         final String zygotePid = result.getStdout().trim().split("\\s+")[0];
145         assertTrue(!zygotePid.isEmpty());
146 
147         final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*boot";
148         return Optional.of(getMappedArtifacts(zygotePid, grepPattern));
149     }
150 
getSystemServerLoadedArtifacts()151     public Set<String> getSystemServerLoadedArtifacts() throws Exception {
152         final CommandResult result =
153                 mTestInfo.getDevice().executeShellV2Command("pidof system_server");
154         assertTrue(result.toString(), result.getExitCode() == 0);
155         final String systemServerPid = result.getStdout().trim();
156         assertTrue(!systemServerPid.isEmpty());
157         assertTrue(
158                 "There should be exactly one `system_server` process",
159                 systemServerPid.matches("\\d+"));
160 
161         // system_server artifacts are in the APEX data dalvik cache and names all contain
162         // the word "@classes". Look for mapped files that match this pattern in the proc map for
163         // system_server.
164         final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*@classes";
165         return getMappedArtifacts(systemServerPid, grepPattern);
166     }
167 
verifyZygoteLoadedArtifacts(String zygoteName, Set<String> mappedArtifacts, String bootImageStem)168     public void verifyZygoteLoadedArtifacts(String zygoteName, Set<String> mappedArtifacts,
169             String bootImageStem) throws Exception {
170         assertTrue("Expect 3 bootclasspath artifacts", mappedArtifacts.size() == 3);
171 
172         String allArtifacts = mappedArtifacts.stream().collect(Collectors.joining(","));
173         for (String extension : BCP_ARTIFACT_EXTENSIONS) {
174             final String artifact = bootImageStem + extension;
175             final boolean found = mappedArtifacts.stream().anyMatch(a -> a.endsWith(artifact));
176             assertTrue(zygoteName + " " + artifact + " not found: '" + allArtifacts + "'", found);
177         }
178     }
179 
180     // Verifies that boot image files with the given stem are loaded by Zygote for each instruction
181     // set. Returns the verified files.
verifyZygotesLoadedArtifacts(String bootImageStem)182     public HashSet<String> verifyZygotesLoadedArtifacts(String bootImageStem) throws Exception {
183         // There are potentially two zygote processes "zygote" and "zygote64". These are
184         // instances 32-bit and 64-bit unspecialized app_process processes.
185         // (frameworks/base/cmds/app_process).
186         int zygoteCount = 0;
187         HashSet<String> verifiedArtifacts = new HashSet<>();
188         for (String zygoteName : ZYGOTE_NAMES) {
189             final Optional<Set<String>> mappedArtifacts = getZygoteLoadedArtifacts(zygoteName);
190             if (!mappedArtifacts.isPresent()) {
191                 continue;
192             }
193             verifyZygoteLoadedArtifacts(zygoteName, mappedArtifacts.get(), bootImageStem);
194             zygoteCount += 1;
195             verifiedArtifacts.addAll(mappedArtifacts.get());
196         }
197         assertTrue("No zygote processes found", zygoteCount > 0);
198         return verifiedArtifacts;
199     }
200 
verifySystemServerLoadedArtifacts()201     public void verifySystemServerLoadedArtifacts() throws Exception {
202         String[] classpathElements = getListFromEnvironmentVariable("SYSTEMSERVERCLASSPATH");
203         assertTrue("SYSTEMSERVERCLASSPATH is empty", classpathElements.length > 0);
204         String[] standaloneJars = getListFromEnvironmentVariable("STANDALONE_SYSTEMSERVER_JARS");
205         String[] allSystemServerJars = Stream
206                 .concat(Arrays.stream(classpathElements), Arrays.stream(standaloneJars))
207                 .toArray(String[]::new);
208 
209         final Set<String> mappedArtifacts = getSystemServerLoadedArtifacts();
210         assertTrue(
211                 "No mapped artifacts under " + ART_APEX_DALVIK_CACHE_DIRNAME,
212                 mappedArtifacts.size() > 0);
213         final String isa = getSystemServerIsa(mappedArtifacts.iterator().next());
214         final String isaCacheDirectory = String.format("%s/%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa);
215 
216         // Check components in the system_server classpath have mapped artifacts.
217         for (String element : allSystemServerJars) {
218           String escapedPath = element.substring(1).replace('/', '@');
219           for (String extension : APP_ARTIFACT_EXTENSIONS) {
220             final String fullArtifactPath =
221                     String.format("%s/%s@classes%s", isaCacheDirectory, escapedPath, extension);
222             assertTrue("Missing " + fullArtifactPath, mappedArtifacts.contains(fullArtifactPath));
223           }
224         }
225 
226         for (String mappedArtifact : mappedArtifacts) {
227           // Check the mapped artifact has a .art, .odex or .vdex extension.
228           final boolean knownArtifactKind =
229                     APP_ARTIFACT_EXTENSIONS.stream().anyMatch(e -> mappedArtifact.endsWith(e));
230           assertTrue("Unknown artifact kind: " + mappedArtifact, knownArtifactKind);
231         }
232     }
233 
haveCompilationLog()234     public boolean haveCompilationLog() throws Exception {
235         CommandResult result =
236                 mTestInfo.getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG);
237         return result.getExitCode() == 0;
238     }
239 
removeCompilationLogToAvoidBackoff()240     public void removeCompilationLogToAvoidBackoff() throws Exception {
241         mTestInfo.getDevice().executeShellCommand("rm -f " + ODREFRESH_COMPILATION_LOG);
242     }
243 
reboot()244     public void reboot() throws Exception {
245         TestDeviceOptions options = mTestInfo.getDevice().getOptions();
246         // store default value and increase time-out for reboot
247         int rebootTimeout = options.getRebootTimeout();
248         long onlineTimeout = options.getOnlineTimeout();
249         options.setRebootTimeout((int)BOOT_COMPLETE_TIMEOUT.toMillis());
250         options.setOnlineTimeout(BOOT_COMPLETE_TIMEOUT.toMillis());
251         mTestInfo.getDevice().setOptions(options);
252 
253         mTestInfo.getDevice().reboot();
254         boolean success =
255                 mTestInfo.getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
256 
257         // restore default values
258         options.setRebootTimeout(rebootTimeout);
259         options.setOnlineTimeout(onlineTimeout);
260         mTestInfo.getDevice().setOptions(options);
261 
262         assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
263     }
264 
restartZygote()265     public void restartZygote() throws Exception {
266         // `waitForBootComplete` relies on `dev.bootcomplete`.
267         mTestInfo.getDevice().executeShellCommand("setprop dev.bootcomplete 0");
268         mTestInfo.getDevice().executeShellCommand("setprop ctl.restart zygote");
269         boolean success = mTestInfo.getDevice()
270                 .waitForBootComplete(RESTART_ZYGOTE_COMPLETE_TIMEOUT.toMillis());
271         assertWithMessage("Zygote didn't start in %s", BOOT_COMPLETE_TIMEOUT).that(success)
272                 .isTrue();
273     }
274 
275     /**
276      * Returns the value of a boolean test property, or false if it does not exist.
277      */
getBooleanOrDefault(String key)278     private boolean getBooleanOrDefault(String key) {
279         String value = mTestInfo.properties().get(key);
280         if (value == null) {
281             return false;
282         }
283         return Boolean.parseBoolean(value);
284     }
285 
setBoolean(String key, boolean value)286     private void setBoolean(String key, boolean value) {
287         mTestInfo.properties().put(key, Boolean.toString(value));
288     }
289 
getListFromEnvironmentVariable(String name)290     private String[] getListFromEnvironmentVariable(String name) throws Exception {
291         String systemServerClasspath =
292                 mTestInfo.getDevice().executeShellCommand("echo $" + name).trim();
293         if (!systemServerClasspath.isEmpty()) {
294             return systemServerClasspath.split(":");
295         }
296         return new String[0];
297     }
298 
getSystemServerIsa(String mappedArtifact)299     private String getSystemServerIsa(String mappedArtifact) {
300         // Artifact path for system server artifacts has the form:
301         //    ART_APEX_DALVIK_CACHE_DIRNAME + "/<arch>/system@framework@some.jar@classes.odex"
302         String[] pathComponents = mappedArtifact.split("/");
303         return pathComponents[pathComponents.length - 2];
304     }
305 
parseFormattedDateTime(String dateTimeStr)306     private long parseFormattedDateTime(String dateTimeStr) throws Exception {
307         DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
308                 "yyyy-MM-dd HH:mm:ss.nnnnnnnnn Z");
309         ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStr, formatter);
310         return zonedDateTime.toInstant().toEpochMilli();
311     }
312 
getModifiedTimeMs(String filename)313     public long getModifiedTimeMs(String filename) throws Exception {
314         // We can't use the "-c '%.3Y'" flag when to get the timestamp because the Toybox's `stat`
315         // implementation truncates the timestamp to seconds, which is not accurate enough, so we
316         // use "-c '%%y'" and parse the time ourselves.
317         String dateTimeStr = mTestInfo.getDevice()
318                 .executeShellCommand(String.format("stat -c '%%y' '%s'", filename))
319                 .trim();
320         return parseFormattedDateTime(dateTimeStr);
321     }
322 
getCurrentTimeMs()323     public long getCurrentTimeMs() throws Exception {
324         // We can't use getDevice().getDeviceDate() because it truncates the timestamp to seconds,
325         // which is not accurate enough.
326         String dateTimeStr = mTestInfo.getDevice()
327                 .executeShellCommand("date +'%Y-%m-%d %H:%M:%S.%N %z'")
328                 .trim();
329         return parseFormattedDateTime(dateTimeStr);
330     }
331 
countFilesCreatedBeforeTime(String directory, long timestampMs)332     public int countFilesCreatedBeforeTime(String directory, long timestampMs)
333             throws DeviceNotAvailableException {
334         // Drop the precision to second, mainly because we need to use `find -newerct` to query
335         // files by timestamp, but toybox can't parse `date +'%s.%N'` currently.
336         String timestamp = String.valueOf(timestampMs / 1000);
337         // For simplicity, directory must be a simple path that doesn't require escaping.
338         String output = assertCommandSucceeds(
339                 "find " + directory + " -type f ! -newerct '@" + timestamp + "' | wc -l");
340         return Integer.parseInt(output);
341     }
342 
countFilesCreatedAfterTime(String directory, long timestampMs)343     public int countFilesCreatedAfterTime(String directory, long timestampMs)
344             throws DeviceNotAvailableException {
345         // Drop the precision to second, mainly because we need to use `find -newerct` to query
346         // files by timestamp, but toybox can't parse `date +'%s.%N'` currently.
347         String timestamp = String.valueOf(timestampMs / 1000);
348         // For simplicity, directory must be a simple path that doesn't require escaping.
349         String output = assertCommandSucceeds(
350                 "find " + directory + " -type f -newerct '@" + timestamp + "' | wc -l");
351         return Integer.parseInt(output);
352     }
353 
assertCommandSucceeds(String command)354     public String assertCommandSucceeds(String command) throws DeviceNotAvailableException {
355         CommandResult result = mTestInfo.getDevice().executeShellV2Command(command);
356         assertWithMessage(result.toString()).that(result.getExitCode()).isEqualTo(0);
357         return result.getStdout().trim();
358     }
359 
archiveLogThenDelete(TestLogData logs, String remotePath, String localName)360     public void archiveLogThenDelete(TestLogData logs, String remotePath, String localName)
361             throws DeviceNotAvailableException {
362         ITestDevice device = mTestInfo.getDevice();
363         File logFile = device.pullFile(remotePath);
364         if (logFile != null) {
365             logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile));
366             // Delete to avoid confusing logs from a previous run, just in case.
367             device.deleteFile(remotePath);
368         }
369     }
370 
371 }
372