1 /*
2  * Copyright (C) 2019 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.rollback.host;
18 
19 import static com.android.tests.rollback.host.WatchdogEventLogger.Subject.assertThat;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import static org.junit.Assert.fail;
25 import static org.junit.Assume.assumeTrue;
26 
27 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
28 import com.android.ddmlib.Log;
29 import com.android.tradefed.device.DeviceNotAvailableException;
30 import com.android.tradefed.device.IFileEntry;
31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
33 import com.android.tradefed.util.CommandResult;
34 import com.android.tradefed.util.CommandStatus;
35 
36 import org.junit.After;
37 import org.junit.Before;
38 import org.junit.Rule;
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 
42 import java.io.File;
43 import java.time.Instant;
44 import java.util.Collections;
45 import java.util.Date;
46 import java.util.List;
47 import java.util.concurrent.TimeUnit;
48 import java.util.stream.Collectors;
49 
50 /**
51  * Runs the staged rollback tests.
52  *
53  * TODO(gavincorkery): Support the verification of logging parents in Watchdog metrics.
54  */
55 @RunWith(DeviceJUnit4ClassRunner.class)
56 public class StagedRollbackTest extends BaseHostJUnit4Test {
57     private static final String TAG = "StagedRollbackTest";
58     private static final int NATIVE_CRASHES_THRESHOLD = 5;
59 
60     /**
61      * Runs the given phase of a test by calling into the device.
62      * Throws an exception if the test phase fails.
63      * <p>
64      * For example, <code>runPhase("testApkOnlyEnableRollback");</code>
65      */
runPhase(String phase)66     private void runPhase(String phase) throws Exception {
67         assertThat(runDeviceTests("com.android.tests.rollback",
68                     "com.android.tests.rollback.StagedRollbackTest",
69                     phase)).isTrue();
70     }
71 
72     private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test";
73     private static final String TESTAPP_A = "com.android.cts.install.lib.testapp.A";
74 
75     private static final String TEST_SUBDIR = "/subdir/";
76 
77     private static final String TEST_FILENAME_1 = "test_file.txt";
78     private static final String TEST_STRING_1 = "hello this is a test";
79     private static final String TEST_FILENAME_2 = "another_file.txt";
80     private static final String TEST_STRING_2 = "this is a different file";
81     private static final String TEST_FILENAME_3 = "also.xyz";
82     private static final String TEST_STRING_3 = "also\n a\n test\n string";
83     private static final String TEST_FILENAME_4 = "one_more.test";
84     private static final String TEST_STRING_4 = "once more unto the test";
85 
86     private static final String REASON_APP_CRASH = "REASON_APP_CRASH";
87     private static final String REASON_NATIVE_CRASH = "REASON_NATIVE_CRASH";
88 
89     private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE";
90     private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED";
91     private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS";
92 
93     private WatchdogEventLogger mLogger = new WatchdogEventLogger();
94 
95     @Rule
96     public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);
97 
98     @Before
setUp()99     public void setUp() throws Exception {
100         deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
101                 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex");
102         runPhase("expireRollbacks");
103         mLogger.start(getDevice());
104         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
105         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
106         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
107     }
108 
109     @After
tearDown()110     public void tearDown() throws Exception {
111         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
112         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
113         getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
114         mLogger.stop();
115         runPhase("expireRollbacks");
116         deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
117                 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
118                 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "*",
119                 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*");
120     }
121 
122     /**
123      * Deletes files and reboots the device if necessary.
124      * @param files the paths of files which might contain wildcards
125      */
deleteFiles(String... files)126     private void deleteFiles(String... files) throws Exception {
127         boolean found = false;
128         for (String file : files) {
129             CommandResult result = getDevice().executeShellV2Command("ls " + file);
130             if (result.getStatus() == CommandStatus.SUCCESS) {
131                 found = true;
132                 break;
133             }
134         }
135 
136         if (found) {
137             try {
138                 getDevice().enableAdbRoot();
139                 getDevice().remountSystemWritable();
140                 for (String file : files) {
141                     getDevice().executeShellCommand("rm -rf " + file);
142                 }
143             } finally {
144                 getDevice().disableAdbRoot();
145             }
146             getDevice().reboot();
147         }
148     }
149 
waitForDeviceNotAvailable(long timeout, TimeUnit unit)150     private void waitForDeviceNotAvailable(long timeout, TimeUnit unit) {
151         assertWithMessage("waitForDeviceNotAvailable() timed out in %s %s", timeout, unit)
152                 .that(getDevice().waitForDeviceNotAvailable(unit.toMillis(timeout))).isTrue();
153     }
154 
155     /**
156      * Tests watchdog triggered staged rollbacks involving only apks.
157      */
158     @Test
testBadApkOnly()159     public void testBadApkOnly() throws Exception {
160         runPhase("testBadApkOnly_Phase1_Install");
161         getDevice().reboot();
162         runPhase("testBadApkOnly_Phase2_VerifyInstall");
163 
164         // Launch the app to crash to trigger rollback
165         startActivity(TESTAPP_A);
166         // Wait for reboot to happen
167         waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
168 
169         getDevice().waitForDeviceAvailable();
170 
171         runPhase("testBadApkOnly_Phase3_VerifyRollback");
172 
173         assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A);
174         assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
175         assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
176     }
177 
178     @Test
testNativeWatchdogTriggersRollback()179     public void testNativeWatchdogTriggersRollback() throws Exception {
180         runPhase("testNativeWatchdogTriggersRollback_Phase1_Install");
181 
182         // Reboot device to activate staged package
183         getDevice().reboot();
184 
185         runPhase("testNativeWatchdogTriggersRollback_Phase2_VerifyInstall");
186 
187         // crash system_server enough times to trigger a rollback
188         crashProcess("system_server", NATIVE_CRASHES_THRESHOLD);
189 
190         // Rollback should be committed automatically now.
191         // Give time for rollback to be committed. This could take a while,
192         // because we need all of the following to happen:
193         // 1. system_server comes back up and boot completes.
194         // 2. Rollback health observer detects updatable crashing signal.
195         // 3. Staged rollback session becomes ready.
196         // 4. Device actually reboots.
197         // So we give a generous timeout here.
198         waitForDeviceNotAvailable(5, TimeUnit.MINUTES);
199         getDevice().waitForDeviceAvailable();
200 
201         // verify rollback committed
202         runPhase("testNativeWatchdogTriggersRollback_Phase3_VerifyRollback");
203 
204         assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_NATIVE_CRASH, null);
205         assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
206         assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
207     }
208 
209     @Test
testNativeWatchdogTriggersRollbackForAll()210     public void testNativeWatchdogTriggersRollbackForAll() throws Exception {
211         // This test requires committing multiple staged rollbacks
212         assumeTrue(isCheckpointSupported());
213 
214         // Install a package with rollback enabled.
215         runPhase("testNativeWatchdogTriggersRollbackForAll_Phase1_InstallA");
216         getDevice().reboot();
217 
218         // Once previous staged install is applied, install another package
219         runPhase("testNativeWatchdogTriggersRollbackForAll_Phase2_InstallB");
220         getDevice().reboot();
221 
222         // Verify the new staged install has also been applied successfully.
223         runPhase("testNativeWatchdogTriggersRollbackForAll_Phase3_VerifyInstall");
224 
225         // crash system_server enough times to trigger a rollback
226         crashProcess("system_server", NATIVE_CRASHES_THRESHOLD);
227 
228         // Rollback should be committed automatically now.
229         // Give time for rollback to be committed. This could take a while,
230         // because we need all of the following to happen:
231         // 1. system_server comes back up and boot completes.
232         // 2. Rollback health observer detects updatable crashing signal.
233         // 3. Staged rollback session becomes ready.
234         // 4. Device actually reboots.
235         // So we give a generous timeout here.
236         waitForDeviceNotAvailable(5, TimeUnit.MINUTES);
237         getDevice().waitForDeviceAvailable();
238 
239         // verify all available rollbacks have been committed
240         runPhase("testNativeWatchdogTriggersRollbackForAll_Phase4_VerifyRollback");
241 
242         assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_NATIVE_CRASH, null);
243         assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
244         assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
245     }
246 
247     /**
248      * Tests rolling back user data where there are multiple rollbacks for that package.
249      */
250     @Test
testPreviouslyAbandonedRollbacks()251     public void testPreviouslyAbandonedRollbacks() throws Exception {
252         runPhase("testPreviouslyAbandonedRollbacks_Phase1_InstallAndAbandon");
253         getDevice().reboot();
254         runPhase("testPreviouslyAbandonedRollbacks_Phase2_Rollback");
255         getDevice().reboot();
256         runPhase("testPreviouslyAbandonedRollbacks_Phase3_VerifyRollback");
257     }
258 
259     /**
260      * Tests we can enable rollback for a allowlisted app.
261      */
262     @Test
testRollbackAllowlistedApp()263     public void testRollbackAllowlistedApp() throws Exception {
264         assumeTrue(hasMainlineModule());
265         runPhase("testRollbackAllowlistedApp_Phase1_Install");
266         getDevice().reboot();
267         runPhase("testRollbackAllowlistedApp_Phase2_VerifyInstall");
268     }
269 
270     @Test
testRollbackDataPolicy()271     public void testRollbackDataPolicy() throws Exception {
272         List<String> before = getSnapshotDirectories("/data/misc_ce/0/rollback");
273 
274         runPhase("testRollbackDataPolicy_Phase1_Install");
275         getDevice().reboot();
276         runPhase("testRollbackDataPolicy_Phase2_Rollback");
277         getDevice().reboot();
278         runPhase("testRollbackDataPolicy_Phase3_VerifyRollback");
279 
280         // Verify snapshots are deleted after restoration
281         List<String> after = getSnapshotDirectories("/data/misc_ce/0/rollback");
282         // Only check directories newly created during the test
283         after.removeAll(before);
284         // There should be only one /data/misc_ce/0/rollback/<rollbackId> created during test
285         assertThat(after).hasSize(1);
286         assertDirectoryIsEmpty(after.get(0));
287     }
288 
289     /**
290      * Tests that userdata of apk-in-apex is restored when apex is rolled back.
291      */
292     @Test
testRollbackApexWithApk()293     public void testRollbackApexWithApk() throws Exception {
294         pushTestApex();
295         runPhase("testRollbackApexWithApk_Phase1_Install");
296         getDevice().reboot();
297         runPhase("testRollbackApexWithApk_Phase2_Rollback");
298         getDevice().reboot();
299         runPhase("testRollbackApexWithApk_Phase3_VerifyRollback");
300     }
301 
302     /**
303      * Tests that RollbackPackageHealthObserver is observing apk-in-apex.
304      */
305     @Test
testRollbackApexWithApkCrashing()306     public void testRollbackApexWithApkCrashing() throws Exception {
307         pushTestApex();
308 
309         // Install an apex with apk that crashes
310         runPhase("testRollbackApexWithApkCrashing_Phase1_Install");
311         getDevice().reboot();
312         // Verify apex was installed and then crash the apk
313         runPhase("testRollbackApexWithApkCrashing_Phase2_Crash");
314         // Launch the app to crash to trigger rollback
315         startActivity(TESTAPP_A);
316         // Wait for reboot to happen
317         waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
318         getDevice().waitForDeviceAvailable();
319         // Verify rollback occurred due to crash of apk-in-apex
320         runPhase("testRollbackApexWithApkCrashing_Phase3_VerifyRollback");
321 
322         assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A);
323         assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
324         assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
325     }
326 
327     /**
328      * Tests that data in DE_sys apex data directory is restored when apex is rolled back.
329      */
330     @Test
testRollbackApexDataDirectories_DeSys()331     public void testRollbackApexDataDirectories_DeSys() throws Exception {
332         List<String> before = getSnapshotDirectories("/data/misc/apexrollback");
333         pushTestApex();
334 
335         // Push files to apex data directory
336         String oldFilePath1 = apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "/" + TEST_FILENAME_1;
337         String oldFilePath2 =
338                 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + TEST_SUBDIR + TEST_FILENAME_2;
339         runAsRoot(() -> {
340             pushString(TEST_STRING_1, oldFilePath1);
341             pushString(TEST_STRING_2, oldFilePath2);
342         });
343 
344         // Install new version of the APEX with rollback enabled
345         runPhase("testRollbackApexDataDirectories_Phase1_Install");
346         getDevice().reboot();
347 
348         // Replace files in data directory
349         String newFilePath3 = apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "/" + TEST_FILENAME_3;
350         String newFilePath4 =
351                 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + TEST_SUBDIR + TEST_FILENAME_4;
352         runAsRoot(() -> {
353             getDevice().deleteFile(oldFilePath1);
354             getDevice().deleteFile(oldFilePath2);
355             pushString(TEST_STRING_3, newFilePath3);
356             pushString(TEST_STRING_4, newFilePath4);
357         });
358 
359         // Roll back the APEX
360         runPhase("testRollbackApexDataDirectories_Phase2_Rollback");
361         getDevice().reboot();
362 
363         // Verify that old files have been restored and new files are gone
364         runAsRoot(() -> {
365             assertFileContents(TEST_STRING_1, oldFilePath1);
366             assertFileContents(TEST_STRING_2, oldFilePath2);
367             assertFileNotExists(newFilePath3);
368             assertFileNotExists(newFilePath4);
369         });
370 
371         // Verify snapshots are deleted after restoration
372         List<String> after = getSnapshotDirectories("/data/misc/apexrollback");
373         // Only check directories newly created during the test
374         after.removeAll(before);
375         // There should be only one /data/misc/apexrollback/<rollbackId> created during test
376         assertThat(after).hasSize(1);
377         assertDirectoryIsEmpty(after.get(0));
378     }
379 
380     /**
381      * Tests that data in DE (user) apex data directory is restored when apex is rolled back.
382      */
383     @Test
testRollbackApexDataDirectories_DeUser()384     public void testRollbackApexDataDirectories_DeUser() throws Exception {
385         List<String> before = getSnapshotDirectories("/data/misc_de/0/apexrollback");
386         pushTestApex();
387 
388         // Push files to apex data directory
389         String oldFilePath1 = apexDataDirDeUser(
390                 APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1;
391         String oldFilePath2 =
392                 apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
393         runAsRoot(() -> {
394             pushString(TEST_STRING_1, oldFilePath1);
395             pushString(TEST_STRING_2, oldFilePath2);
396         });
397 
398         // Install new version of the APEX with rollback enabled
399         runPhase("testRollbackApexDataDirectories_Phase1_Install");
400         getDevice().reboot();
401 
402         // Replace files in data directory
403         String newFilePath3 =
404                 apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_3;
405         String newFilePath4 =
406                 apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4;
407         runAsRoot(() -> {
408             getDevice().deleteFile(oldFilePath1);
409             getDevice().deleteFile(oldFilePath2);
410             pushString(TEST_STRING_3, newFilePath3);
411             pushString(TEST_STRING_4, newFilePath4);
412         });
413 
414         // Roll back the APEX
415         runPhase("testRollbackApexDataDirectories_Phase2_Rollback");
416         getDevice().reboot();
417 
418         // Verify that old files have been restored and new files are gone
419         runAsRoot(() -> {
420             assertFileContents(TEST_STRING_1, oldFilePath1);
421             assertFileContents(TEST_STRING_2, oldFilePath2);
422             assertFileNotExists(newFilePath3);
423             assertFileNotExists(newFilePath4);
424         });
425 
426         // Verify snapshots are deleted after restoration
427         List<String> after = getSnapshotDirectories("/data/misc_de/0/apexrollback");
428         // Only check directories newly created during the test
429         after.removeAll(before);
430         // There should be only one /data/misc_de/0/apexrollback/<rollbackId> created during test
431         assertThat(after).hasSize(1);
432         assertDirectoryIsEmpty(after.get(0));
433     }
434 
435     /**
436      * Tests that data in CE apex data directory is restored when apex is rolled back.
437      */
438     @Test
testRollbackApexDataDirectories_Ce()439     public void testRollbackApexDataDirectories_Ce() throws Exception {
440         List<String> before = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
441         pushTestApex();
442 
443         // Push files to apex data directory
444         String oldFilePath1 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1;
445         String oldFilePath2 =
446                 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
447         runAsRoot(() -> {
448             pushString(TEST_STRING_1, oldFilePath1);
449             pushString(TEST_STRING_2, oldFilePath2);
450         });
451 
452         // Install new version of the APEX with rollback enabled
453         runPhase("testRollbackApexDataDirectories_Phase1_Install");
454         getDevice().reboot();
455 
456         // Replace files in data directory
457         String newFilePath3 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_3;
458         String newFilePath4 =
459                 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4;
460         runAsRoot(() -> {
461             getDevice().deleteFile(oldFilePath1);
462             getDevice().deleteFile(oldFilePath2);
463             pushString(TEST_STRING_3, newFilePath3);
464             pushString(TEST_STRING_4, newFilePath4);
465         });
466 
467         // Roll back the APEX
468         runPhase("testRollbackApexDataDirectories_Phase2_Rollback");
469         getDevice().reboot();
470 
471         // Verify that old files have been restored and new files are gone
472         runAsRoot(() -> {
473             assertFileContents(TEST_STRING_1, oldFilePath1);
474             assertFileContents(TEST_STRING_2, oldFilePath2);
475             assertFileNotExists(newFilePath3);
476             assertFileNotExists(newFilePath4);
477         });
478 
479         // Verify snapshots are deleted after restoration
480         List<String> after = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
481         // Only check directories newly created during the test
482         after.removeAll(before);
483         // There should be only one /data/misc_ce/0/apexrollback/<rollbackId> created during test
484         assertThat(after).hasSize(1);
485         assertDirectoryIsEmpty(after.get(0));
486     }
487 
488     /**
489      * Tests that data in DE apk data directory is restored when apk is rolled back.
490      */
491     @Test
testRollbackApkDataDirectories_De()492     public void testRollbackApkDataDirectories_De() throws Exception {
493         // Install version 1 of TESTAPP_A
494         runPhase("testRollbackApkDataDirectories_Phase1_InstallV1");
495 
496         // Push files to apk data directory
497         String oldFilePath1 = apkDataDirDe(TESTAPP_A, 0) + "/" + TEST_FILENAME_1;
498         String oldFilePath2 = apkDataDirDe(TESTAPP_A, 0) + TEST_SUBDIR + TEST_FILENAME_2;
499         runAsRoot(() -> {
500             pushString(TEST_STRING_1, oldFilePath1);
501             pushString(TEST_STRING_2, oldFilePath2);
502         });
503 
504         // Install version 2 of TESTAPP_A with rollback enabled
505         runPhase("testRollbackApkDataDirectories_Phase2_InstallV2");
506         getDevice().reboot();
507 
508         // Replace files in data directory
509         String newFilePath3 = apkDataDirDe(TESTAPP_A, 0) + "/" + TEST_FILENAME_3;
510         String newFilePath4 = apkDataDirDe(TESTAPP_A, 0) + TEST_SUBDIR + TEST_FILENAME_4;
511         runAsRoot(() -> {
512             getDevice().deleteFile(oldFilePath1);
513             getDevice().deleteFile(oldFilePath2);
514             pushString(TEST_STRING_3, newFilePath3);
515             pushString(TEST_STRING_4, newFilePath4);
516         });
517 
518         // Roll back the APK
519         runPhase("testRollbackApkDataDirectories_Phase3_Rollback");
520         getDevice().reboot();
521 
522         // Verify that old files have been restored and new files are gone
523         runAsRoot(() -> {
524             assertFileContents(TEST_STRING_1, oldFilePath1);
525             assertFileContents(TEST_STRING_2, oldFilePath2);
526             assertFileNotExists(newFilePath3);
527             assertFileNotExists(newFilePath4);
528         });
529     }
530 
531     @Test
testExpireApexRollback()532     public void testExpireApexRollback() throws Exception {
533         List<String> before = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
534         pushTestApex();
535 
536         // Push files to apex data directory
537         String oldFilePath1 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1;
538         String oldFilePath2 =
539                 apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2;
540         runAsRoot(() -> {
541             pushString(TEST_STRING_1, oldFilePath1);
542             pushString(TEST_STRING_2, oldFilePath2);
543         });
544 
545         // Install new version of the APEX with rollback enabled
546         runPhase("testRollbackApexDataDirectories_Phase1_Install");
547         getDevice().reboot();
548 
549         List<String> after = getSnapshotDirectories("/data/misc_ce/0/apexrollback");
550         // Only check directories newly created during the test
551         after.removeAll(before);
552         // There should be only one /data/misc_ce/0/apexrollback/<rollbackId> created during test
553         assertThat(after).hasSize(1);
554         // Expire all rollbacks and check CE snapshot directories are deleted
555         runPhase("expireRollbacks");
556         runAsRoot(() -> {
557             for (String dir : after) {
558                 assertFileNotExists(dir);
559             }
560         });
561     }
562 
563     /**
564      * Tests that packages are monitored across multiple reboots.
565      */
566     @Test
testWatchdogMonitorsAcrossReboots()567     public void testWatchdogMonitorsAcrossReboots() throws Exception {
568         runPhase("testWatchdogMonitorsAcrossReboots_Phase1_Install");
569 
570         // The first reboot will make the rollback available.
571         // Information about which packages are monitored will be persisted to a file before the
572         // second reboot, and read from disk after the second reboot.
573         getDevice().reboot();
574         getDevice().reboot();
575 
576         runPhase("testWatchdogMonitorsAcrossReboots_Phase2_VerifyInstall");
577 
578         // Launch the app to crash to trigger rollback
579         startActivity(TESTAPP_A);
580         // Wait for reboot to happen
581         waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
582         getDevice().waitForDeviceAvailable();
583 
584         runPhase("testWatchdogMonitorsAcrossReboots_Phase3_VerifyRollback");
585     }
586 
587     /**
588      * Tests an available rollback shouldn't be deleted when its session expires.
589      */
590     @Test
testExpireSession()591     public void testExpireSession() throws Exception {
592         runPhase("testExpireSession_Phase1_Install");
593         getDevice().reboot();
594         runPhase("testExpireSession_Phase2_VerifyInstall");
595 
596         // Advance system clock by 7 days to expire the staged session
597         Instant t1 = Instant.ofEpochMilli(getDevice().getDeviceDate());
598         Instant t2 = t1.plusMillis(TimeUnit.DAYS.toMillis(7));
599         runAsRoot(() -> getDevice().setDate(Date.from(t2)));
600 
601         // Somehow we need to wait for a while before reboot. Otherwise the change to the
602         // system clock will be reset after reboot.
603         Thread.sleep(3000);
604         getDevice().reboot();
605         runPhase("testExpireSession_Phase3_VerifyRollback");
606     }
607 
pushTestApex()608     private void pushTestApex() throws Exception {
609         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
610         final String fileName = APK_IN_APEX_TESTAPEX_NAME + "_v1.apex";
611         final File apex = buildHelper.getTestFile(fileName);
612         try {
613             getDevice().enableAdbRoot();
614             getDevice().remountSystemWritable();
615             assertThat(getDevice().pushFile(apex, "/system/apex/" + fileName)).isTrue();
616         } finally {
617             getDevice().disableAdbRoot();
618         }
619         getDevice().reboot();
620     }
621 
pushString(String contents, String path)622     private void pushString(String contents, String path) throws Exception {
623         assertWithMessage("Failed to push file to device, content=%s path=%s", contents, path)
624                 .that(getDevice().pushString(contents, path)).isTrue();
625     }
626 
assertFileContents(String expectedContents, String path)627     private void assertFileContents(String expectedContents, String path) throws Exception {
628         String actualContents = getDevice().pullFileContents(path);
629         assertWithMessage("Failed to retrieve file=%s", path).that(actualContents).isNotNull();
630         assertWithMessage("Mismatched file contents, path=%s", path)
631                 .that(actualContents).isEqualTo(expectedContents);
632     }
633 
assertFileNotExists(String path)634     private void assertFileNotExists(String path) throws Exception {
635         assertWithMessage("File shouldn't exist, path=%s", path)
636                 .that(getDevice().getFileEntry(path)).isNull();
637     }
638 
apexDataDirDeSys(String apexName)639     private static String apexDataDirDeSys(String apexName) {
640         return String.format("/data/misc/apexdata/%s", apexName);
641     }
642 
apexDataDirDeUser(String apexName, int userId)643     private static String apexDataDirDeUser(String apexName, int userId) {
644         return String.format("/data/misc_de/%d/apexdata/%s", userId, apexName);
645     }
646 
apexDataDirCe(String apexName, int userId)647     private static String apexDataDirCe(String apexName, int userId) {
648         return String.format("/data/misc_ce/%d/apexdata/%s", userId, apexName);
649     }
650 
apkDataDirDe(String apexName, int userId)651     private static String apkDataDirDe(String apexName, int userId) {
652         return String.format("/data/user_de/%d/%s", userId, apexName);
653     }
654 
getSnapshotDirectories(String baseDir)655     private List<String> getSnapshotDirectories(String baseDir) throws Exception {
656         try {
657             getDevice().enableAdbRoot();
658             IFileEntry f = getDevice().getFileEntry(baseDir);
659             if (f == null) {
660                 Log.d(TAG, "baseDir doesn't exist: " + baseDir);
661                 return Collections.EMPTY_LIST;
662             }
663             List<String> list = f.getChildren(false)
664                     .stream().filter(entry -> entry.getName().matches("\\d+(-prerestore)?"))
665                     .map(entry -> entry.getFullPath())
666                     .collect(Collectors.toList());
667             Log.d(TAG, "getSnapshotDirectories=" + list);
668             return list;
669         } finally {
670             getDevice().disableAdbRoot();
671         }
672     }
673 
assertDirectoryIsEmpty(String path)674     private void assertDirectoryIsEmpty(String path) throws Exception {
675         try {
676             getDevice().enableAdbRoot();
677             IFileEntry file = getDevice().getFileEntry(path);
678             assertWithMessage("Not a directory: " + path).that(file.isDirectory()).isTrue();
679             assertWithMessage("Directory not empty: " + path)
680                     .that(file.getChildren(false)).isEmpty();
681         } catch (DeviceNotAvailableException e) {
682             fail("Can't access directory: " + path);
683         } finally {
684             getDevice().disableAdbRoot();
685         }
686     }
687 
startActivity(String packageName)688     private void startActivity(String packageName) throws Exception {
689         String cmd = "am start -S -a android.intent.action.MAIN "
690                 + "-c android.intent.category.LAUNCHER " + packageName;
691         getDevice().executeShellCommand(cmd);
692     }
693 
crashProcess(String processName, int numberOfCrashes)694     private void crashProcess(String processName, int numberOfCrashes) throws Exception {
695         String pid = "";
696         String lastPid = "invalid";
697         for (int i = 0; i < numberOfCrashes; ++i) {
698             // This condition makes sure before we kill the process, the process is running AND
699             // the last crash was finished.
700             while ("".equals(pid) || lastPid.equals(pid)) {
701                 pid = getDevice().executeShellCommand("pidof " + processName);
702             }
703             getDevice().executeShellCommand("kill " + pid);
704             lastPid = pid;
705         }
706     }
707 
isCheckpointSupported()708     private boolean isCheckpointSupported() throws Exception {
709         try {
710             runPhase("isCheckpointSupported");
711             return true;
712         } catch (AssertionError ignore) {
713             return false;
714         }
715     }
716 
717     /**
718      * True if this build has mainline modules installed.
719      */
hasMainlineModule()720     private boolean hasMainlineModule() throws Exception {
721         try {
722             runPhase("hasMainlineModule");
723             return true;
724         } catch (AssertionError ignore) {
725             return false;
726         }
727     }
728 
729     @FunctionalInterface
730     private interface ExceptionalRunnable {
run()731         void run() throws Exception;
732     }
733 
runAsRoot(ExceptionalRunnable runnable)734     private void runAsRoot(ExceptionalRunnable runnable) throws Exception {
735         try {
736             getDevice().enableAdbRoot();
737             runnable.run();
738         } finally {
739             getDevice().disableAdbRoot();
740         }
741     }
742 }
743