1 /* 2 * Copyright (C) 2017 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.server.pm; 18 19 import android.app.AlarmManager; 20 import android.content.Context; 21 import android.os.Environment; 22 import android.os.ParcelFileDescriptor; 23 import android.os.SystemProperties; 24 import android.os.storage.StorageManager; 25 import android.util.Log; 26 27 import androidx.test.InstrumentationRegistry; 28 29 import org.junit.After; 30 import org.junit.Assert; 31 import org.junit.Before; 32 import org.junit.BeforeClass; 33 import org.junit.Test; 34 import org.junit.runner.RunWith; 35 import org.junit.runners.JUnit4; 36 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.io.InputStreamReader; 42 import java.util.concurrent.TimeUnit; 43 44 /** 45 * Integration tests for {@link BackgroundDexOptService}. 46 * 47 * Tests various scenarios around BackgroundDexOptService. 48 * 1. Under normal conditions, check that dexopt upgrades test app to 49 * $(getprop pm.dexopt.bg-dexopt). 50 * 2. Under low storage conditions and package is unused, check 51 * that dexopt downgrades test app to $(getprop pm.dexopt.inactive). 52 * 3. Under low storage conditions and package is recently used, check 53 * that dexopt upgrades test app to $(getprop pm.dexopt.bg-dexopt). 54 * 55 * Each test case runs "cmd package bg-dexopt-job com.android.frameworks.bgdexopttest". 56 * 57 * The setup for these tests make sure this package has been configured to have been recently used 58 * plus installed far enough in the past. If a test case requires that this package has not been 59 * recently used, it sets the time forward more than 60 * `getprop pm.dexopt.downgrade_after_inactive_days` days. 61 * 62 * For tests that require low storage, the phone is filled up. 63 * 64 * Run with "atest BackgroundDexOptServiceIntegrationTests". 65 */ 66 @RunWith(JUnit4.class) 67 public final class BackgroundDexOptServiceIntegrationTests { 68 69 private static final String TAG = BackgroundDexOptServiceIntegrationTests.class.getSimpleName(); 70 71 // Name of package to test on. 72 private static final String PACKAGE_NAME = "com.android.frameworks.bgdexopttest"; 73 // Name of file used to fill up storage. 74 private static final String BIG_FILE = "bigfile"; 75 private static final String BG_DEXOPT_COMPILER_FILTER = SystemProperties.get( 76 "pm.dexopt.bg-dexopt"); 77 private static final String DOWNGRADE_COMPILER_FILTER = SystemProperties.get( 78 "pm.dexopt.inactive"); 79 private static final long DOWNGRADE_AFTER_DAYS = SystemProperties.getLong( 80 "pm.dexopt.downgrade_after_inactive_days", 0); 81 // Needs to be between 1.0 and 2.0. 82 private static final double LOW_STORAGE_MULTIPLIER = 1.5; 83 84 // The file used to fill up storage. 85 private File mBigFile; 86 87 // Remember start time. 88 @BeforeClass setUpAll()89 public static void setUpAll() { 90 if (!SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false)) { 91 throw new RuntimeException( 92 "bg-dexopt is not disabled (set pm.dexopt.disable_bg_dexopt to true)"); 93 } 94 if (DOWNGRADE_AFTER_DAYS < 1) { 95 throw new RuntimeException( 96 "pm.dexopt.downgrade_after_inactive_days must be at least 1"); 97 } 98 if ("quicken".equals(BG_DEXOPT_COMPILER_FILTER)) { 99 throw new RuntimeException("pm.dexopt.bg-dexopt should not be \"quicken\""); 100 } 101 if ("quicken".equals(DOWNGRADE_COMPILER_FILTER)) { 102 throw new RuntimeException("pm.dexopt.inactive should not be \"quicken\""); 103 } 104 } 105 106 getContext()107 private static Context getContext() { 108 return InstrumentationRegistry.getTargetContext(); 109 } 110 111 @Before setUp()112 public void setUp() throws IOException { 113 File dataDir = getContext().getDataDir(); 114 mBigFile = new File(dataDir, BIG_FILE); 115 } 116 117 @After tearDown()118 public void tearDown() { 119 if (mBigFile.exists()) { 120 boolean result = mBigFile.delete(); 121 if (!result) { 122 throw new RuntimeException("Couldn't delete big file"); 123 } 124 } 125 } 126 127 // Return the content of the InputStream as a String. inputStreamToString(InputStream is)128 private static String inputStreamToString(InputStream is) throws IOException { 129 char[] buffer = new char[1024]; 130 StringBuilder builder = new StringBuilder(); 131 try (InputStreamReader reader = new InputStreamReader(is)) { 132 for (; ; ) { 133 int count = reader.read(buffer, 0, buffer.length); 134 if (count < 0) { 135 break; 136 } 137 builder.append(buffer, 0, count); 138 } 139 } 140 return builder.toString(); 141 } 142 143 // Run the command and return the stdout. runShellCommand(String cmd)144 private static String runShellCommand(String cmd) throws IOException { 145 Log.i(TAG, String.format("running command: '%s'", cmd)); 146 ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation() 147 .executeShellCommand(cmd); 148 byte[] buf = new byte[512]; 149 int bytesRead; 150 FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd); 151 StringBuilder stdout = new StringBuilder(); 152 while ((bytesRead = fis.read(buf)) != -1) { 153 stdout.append(new String(buf, 0, bytesRead)); 154 } 155 fis.close(); 156 Log.i(TAG, "stdout"); 157 Log.i(TAG, stdout.toString()); 158 return stdout.toString(); 159 } 160 161 // Run the command and return the stdout split by lines. runShellCommandSplitLines(String cmd)162 private static String[] runShellCommandSplitLines(String cmd) throws IOException { 163 return runShellCommand(cmd).split("\n"); 164 } 165 166 // Return the compiler filter of a package. getCompilerFilter(String pkg)167 private static String getCompilerFilter(String pkg) throws IOException { 168 String cmd = String.format("dumpsys package %s", pkg); 169 String[] lines = runShellCommandSplitLines(cmd); 170 final String substr = "[status="; 171 for (String line : lines) { 172 int startIndex = line.indexOf(substr); 173 if (startIndex < 0) { 174 continue; 175 } 176 startIndex += substr.length(); 177 int endIndex = line.indexOf(']', startIndex); 178 return line.substring(startIndex, endIndex); 179 } 180 throw new RuntimeException("Couldn't find compiler filter in dumpsys package"); 181 } 182 183 // Return the number of bytes available in the data partition. getDataDirUsableSpace()184 private static long getDataDirUsableSpace() { 185 return Environment.getDataDirectory().getUsableSpace(); 186 } 187 188 // Fill up the storage until there are bytesRemaining number of bytes available in the data 189 // partition. Writes to the current package's data directory. fillUpStorage(long bytesRemaining)190 private void fillUpStorage(long bytesRemaining) throws IOException { 191 Log.i(TAG, String.format("Filling up storage with %d bytes remaining", bytesRemaining)); 192 logSpaceRemaining(); 193 long numBytesToAdd = getDataDirUsableSpace() - bytesRemaining; 194 String cmd = String.format("fallocate -l %d %s", numBytesToAdd, mBigFile.getAbsolutePath()); 195 runShellCommand(cmd); 196 logSpaceRemaining(); 197 } 198 199 // Fill up storage so that device is in low storage condition. fillUpToLowStorage()200 private void fillUpToLowStorage() throws IOException { 201 fillUpStorage((long) (getStorageLowBytes() * LOW_STORAGE_MULTIPLIER)); 202 } 203 204 // TODO(aeubanks): figure out how to get scheduled bg-dexopt to run runBackgroundDexOpt()205 private static void runBackgroundDexOpt() throws IOException { 206 String result = runShellCommand("cmd package bg-dexopt-job " + PACKAGE_NAME); 207 if (!result.trim().equals("Success")) { 208 throw new IllegalStateException("Expected command success, received >" + result + "<"); 209 } 210 } 211 212 // Set the time ahead of the last use time of the test app in days. setTimeFutureDays(long futureDays)213 private static void setTimeFutureDays(long futureDays) { 214 setTimeFutureMillis(TimeUnit.DAYS.toMillis(futureDays)); 215 } 216 217 // Set the time ahead of the last use time of the test app in milliseconds. setTimeFutureMillis(long futureMillis)218 private static void setTimeFutureMillis(long futureMillis) { 219 long currentTime = System.currentTimeMillis(); 220 setTime(currentTime + futureMillis); 221 } 222 setTime(long time)223 private static void setTime(long time) { 224 AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); 225 am.setTime(time); 226 } 227 228 // Return the number of free bytes when the data partition is considered low on storage. getStorageLowBytes()229 private static long getStorageLowBytes() { 230 StorageManager storageManager = (StorageManager) getContext().getSystemService( 231 Context.STORAGE_SERVICE); 232 return storageManager.getStorageLowBytes(Environment.getDataDirectory()); 233 } 234 235 // Log the amount of space remaining in the data directory. logSpaceRemaining()236 private static void logSpaceRemaining() throws IOException { 237 runShellCommand("df -h /data"); 238 } 239 240 // Compile the given package with the given compiler filter. compilePackageWithFilter(String pkg, String filter)241 private static void compilePackageWithFilter(String pkg, String filter) throws IOException { 242 runShellCommand(String.format("cmd package compile -f -m %s %s", filter, pkg)); 243 } 244 245 // Test that background dexopt under normal conditions succeeds. 246 @Test testBackgroundDexOpt()247 public void testBackgroundDexOpt() throws IOException { 248 // Set filter to quicken. 249 compilePackageWithFilter(PACKAGE_NAME, "verify"); 250 Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME)); 251 252 runBackgroundDexOpt(); 253 254 // Verify that bg-dexopt is successful. 255 Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME)); 256 } 257 258 // Test that background dexopt under low storage conditions upgrades used packages. 259 @Test testBackgroundDexOptDowngradeSkipRecentlyUsedPackage()260 public void testBackgroundDexOptDowngradeSkipRecentlyUsedPackage() throws IOException { 261 // Should be less than DOWNGRADE_AFTER_DAYS. 262 long deltaDays = DOWNGRADE_AFTER_DAYS - 1; 263 try { 264 // Set time to future. 265 setTimeFutureDays(deltaDays); 266 267 // Set filter to quicken. 268 compilePackageWithFilter(PACKAGE_NAME, "quicken"); 269 Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME)); 270 271 // Fill up storage to trigger low storage threshold. 272 fillUpToLowStorage(); 273 274 runBackgroundDexOpt(); 275 276 // Verify that downgrade did not happen. 277 Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME)); 278 } finally { 279 // Reset time. 280 setTimeFutureDays(-deltaDays); 281 } 282 } 283 284 // Test that background dexopt under low storage conditions downgrades unused packages. 285 @Test testBackgroundDexOptDowngradeSuccessful()286 public void testBackgroundDexOptDowngradeSuccessful() throws IOException { 287 // Should be more than DOWNGRADE_AFTER_DAYS. 288 long deltaDays = DOWNGRADE_AFTER_DAYS + 1; 289 try { 290 // Set time to future. 291 setTimeFutureDays(deltaDays); 292 293 // Set filter to quicken. 294 compilePackageWithFilter(PACKAGE_NAME, "quicken"); 295 Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME)); 296 297 // Fill up storage to trigger low storage threshold. 298 fillUpToLowStorage(); 299 300 runBackgroundDexOpt(); 301 302 // Verify that downgrade is successful. 303 Assert.assertEquals(DOWNGRADE_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME)); 304 } finally { 305 // Reset time. 306 setTimeFutureDays(-deltaDays); 307 } 308 } 309 310 } 311