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