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