1 /*
2  * Copyright (C) 2020 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.internal.util.test;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertTrue;
21 
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.ITestDevice;
24 import com.android.tradefed.log.LogUtil;
25 
26 import org.junit.Assert;
27 import org.junit.ClassRule;
28 import org.junit.rules.ExternalResource;
29 import org.junit.rules.TemporaryFolder;
30 import org.junit.rules.TestRule;
31 import org.junit.runner.Description;
32 import org.junit.runners.model.Statement;
33 
34 import java.io.File;
35 import java.io.FileOutputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.nio.file.Path;
39 import java.nio.file.Paths;
40 import java.util.ArrayList;
41 
42 import javax.annotation.Nullable;
43 
44 /**
45  * Allows pushing files onto the device and various options for rebooting. Useful for installing
46  * APKs/files to system partitions which otherwise wouldn't be easily changed.
47  *
48  * It's strongly recommended to pass in a {@link ClassRule} annotated {@link TestRuleDelegate} to
49  * do a full reboot at the end of a test to ensure the device is in a valid state, assuming the
50  * default {@link RebootStrategy#FULL} isn't used.
51  */
52 public class SystemPreparer extends ExternalResource {
53     private static final long OVERLAY_ENABLE_TIMEOUT_MS = 30000;
54 
55     // The paths of the files pushed onto the device through this rule to be removed after.
56     private ArrayList<String> mPushedFiles = new ArrayList<>();
57 
58     // The package names of packages installed through this rule.
59     private ArrayList<String> mInstalledPackages = new ArrayList<>();
60 
61     private final TemporaryFolder mHostTempFolder;
62     private final DeviceProvider mDeviceProvider;
63     private final RebootStrategy mRebootStrategy;
64     private final TearDownRule mTearDownRule;
65 
66     // When debugging, it may be useful to run a test case without rebooting the device afterwards,
67     // to manually verify the device state.
68     private boolean mDebugSkipAfterReboot;
69 
SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider)70     public SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider) {
71         this(hostTempFolder, RebootStrategy.FULL, null, deviceProvider);
72     }
73 
SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, @Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider)74     public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy,
75             @Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider) {
76         this(hostTempFolder, rebootStrategy, testRuleDelegate, false, deviceProvider);
77     }
78 
SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy, @Nullable TestRuleDelegate testRuleDelegate, boolean debugSkipAfterReboot, DeviceProvider deviceProvider)79     public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy,
80             @Nullable TestRuleDelegate testRuleDelegate, boolean debugSkipAfterReboot,
81             DeviceProvider deviceProvider) {
82         mHostTempFolder = hostTempFolder;
83         mDeviceProvider = deviceProvider;
84         mRebootStrategy = rebootStrategy;
85         mTearDownRule = new TearDownRule(mDeviceProvider);
86         if (testRuleDelegate != null) {
87             testRuleDelegate.setDelegate(mTearDownRule);
88         }
89         mDebugSkipAfterReboot = debugSkipAfterReboot;
90     }
91 
92     /** Copies a file within the host test jar to a path on device. */
pushResourceFile(String filePath, String outputPath)93     public SystemPreparer pushResourceFile(String filePath, String outputPath)
94             throws DeviceNotAvailableException, IOException {
95         final ITestDevice device = mDeviceProvider.getDevice();
96         remount();
97         assertTrue(device.pushFile(copyResourceToTemp(filePath), outputPath));
98         addPushedFile(device, outputPath);
99         return this;
100     }
101 
102     /** Copies a file directly from the host file system to a path on device. */
pushFile(File file, String outputPath)103     public SystemPreparer pushFile(File file, String outputPath)
104             throws DeviceNotAvailableException {
105         final ITestDevice device = mDeviceProvider.getDevice();
106         remount();
107         assertTrue(device.pushFile(file, outputPath));
108         addPushedFile(device, outputPath);
109         return this;
110     }
111 
addPushedFile(ITestDevice device, String outputPath)112     private void addPushedFile(ITestDevice device, String outputPath)
113             throws DeviceNotAvailableException {
114         Path pathCreated = Paths.get(outputPath);
115 
116         // Find the top most parent that is new to the device
117         while (pathCreated.getParent() != null
118                 && !device.doesFileExist(pathCreated.getParent().toString())) {
119             pathCreated = pathCreated.getParent();
120         }
121 
122         mPushedFiles.add(pathCreated.toString());
123     }
124 
125     /** Deletes the given path from the device */
deleteFile(String file)126     public SystemPreparer deleteFile(String file) throws DeviceNotAvailableException {
127         final ITestDevice device = mDeviceProvider.getDevice();
128         remount();
129         device.deleteFile(file);
130         return this;
131     }
132 
133     /** Installs an APK within the host test jar onto the device. */
installResourceApk(String resourcePath, String packageName)134     public SystemPreparer installResourceApk(String resourcePath, String packageName)
135             throws DeviceNotAvailableException, IOException {
136         final ITestDevice device = mDeviceProvider.getDevice();
137         final File tmpFile = copyResourceToTemp(resourcePath);
138         final String result = device.installPackage(tmpFile, true /* reinstall */);
139         Assert.assertNull(result);
140         mInstalledPackages.add(packageName);
141         return this;
142     }
143 
144     /** Stages multiple APEXs within the host test jar onto the device. */
stageMultiplePackages(String[] resourcePaths, String[] packageNames)145     public SystemPreparer stageMultiplePackages(String[] resourcePaths, String[] packageNames)
146             throws DeviceNotAvailableException, IOException {
147         assertEquals(resourcePaths.length, packageNames.length);
148         final ITestDevice device = mDeviceProvider.getDevice();
149         final String[] adbCommandLine = new String[resourcePaths.length + 2];
150         adbCommandLine[0] = "install-multi-package";
151         adbCommandLine[1] = "--staged";
152         for (int i = 0; i < resourcePaths.length; i++) {
153             final File tmpFile = copyResourceToTemp(resourcePaths[i]);
154             adbCommandLine[i + 2] = tmpFile.getAbsolutePath();
155             mInstalledPackages.add(packageNames[i]);
156         }
157         final String output = device.executeAdbCommand(adbCommandLine);
158         assertTrue(output.contains("Success. Reboot device to apply staged session"));
159         return this;
160     }
161 
162     /** Sets the enable state of an overlay package. */
setOverlayEnabled(String packageName, boolean enabled)163     public SystemPreparer setOverlayEnabled(String packageName, boolean enabled)
164             throws DeviceNotAvailableException {
165         final ITestDevice device = mDeviceProvider.getDevice();
166         final String enable = enabled ? "enable" : "disable";
167 
168         // Wait for the overlay to change its enabled state.
169         final long endMillis = System.currentTimeMillis() + OVERLAY_ENABLE_TIMEOUT_MS;
170         String result;
171         while (System.currentTimeMillis() <= endMillis) {
172             device.executeShellCommand(String.format("cmd overlay %s %s", enable, packageName));
173             result = device.executeShellCommand("cmd overlay dump isenabled "
174                     + packageName);
175             if (((enabled) ? "true\n" : "false\n").equals(result)) {
176                 return this;
177             }
178 
179             try {
180                 Thread.sleep(200);
181             } catch (InterruptedException ignore) {
182             }
183         }
184 
185         throw new IllegalStateException(String.format("Failed to %s overlay %s:\n%s", enable,
186                 packageName, device.executeShellCommand("cmd overlay list")));
187     }
188 
189     /** Restarts the device and waits until after boot is completed. */
reboot()190     public SystemPreparer reboot() throws DeviceNotAvailableException {
191         ITestDevice device = mDeviceProvider.getDevice();
192         switch (mRebootStrategy) {
193             case FULL:
194                 device.reboot();
195                 break;
196             case UNTIL_ONLINE:
197                 device.rebootUntilOnline();
198                 break;
199             case USERSPACE:
200                 device.rebootUserspace();
201                 break;
202             case USERSPACE_UNTIL_ONLINE:
203                 device.rebootUserspaceUntilOnline();
204                 break;
205             // TODO(b/159540015): Make this START_STOP instead of default once it's fixed. Can't
206             //  currently be done because START_STOP is commented out.
207             default:
208                 device.executeShellCommand("stop");
209                 device.executeShellCommand("start");
210                 ITestDevice.RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
211                 device.setRecoveryMode(ITestDevice.RecoveryMode.ONLINE);
212 
213                 if (device.isEncryptionSupported()) {
214                     if (device.isDeviceEncrypted()) {
215                         LogUtil.CLog.e("Device is encrypted after userspace reboot!");
216                         device.unlockDevice();
217                     }
218                 }
219 
220                 device.setRecoveryMode(cachedRecoveryMode);
221                 device.waitForDeviceAvailable();
222                 break;
223         }
224         return this;
225     }
226 
remount()227     public SystemPreparer remount() throws DeviceNotAvailableException {
228         mTearDownRule.remount();
229         return this;
230     }
231 
getFileExtension(@ullable String path)232     private static @Nullable String getFileExtension(@Nullable String path) {
233         if (path == null) {
234             return null;
235         }
236         final int lastDot = path.lastIndexOf('.');
237         if (lastDot >= 0) {
238             return path.substring(lastDot + 1);
239         } else {
240             return null;
241         }
242     }
243 
244     /** Copies a file within the host test jar to a temporary file on the host machine. */
copyResourceToTemp(String resourcePath)245     private File copyResourceToTemp(String resourcePath) throws IOException {
246         final String ext = getFileExtension(resourcePath);
247         final File tempFile;
248         if (ext != null) {
249             tempFile = File.createTempFile("junit", "." + ext, mHostTempFolder.getRoot());
250         } else {
251             tempFile = mHostTempFolder.newFile();
252         }
253         final ClassLoader classLoader = getClass().getClassLoader();
254         try (InputStream assetIs = classLoader.getResourceAsStream(resourcePath);
255              FileOutputStream assetOs = new FileOutputStream(tempFile)) {
256             if (assetIs == null) {
257                 throw new IllegalStateException("Failed to find resource " + resourcePath);
258             }
259 
260             int b;
261             while ((b = assetIs.read()) >= 0) {
262                 assetOs.write(b);
263             }
264         }
265 
266         return tempFile;
267     }
268 
269     /** Removes installed packages and files that were pushed to the device. */
270     @Override
after()271     public void after() {
272         final ITestDevice device = mDeviceProvider.getDevice();
273         try {
274             remount();
275             for (final String file : mPushedFiles) {
276                 device.deleteFile(file);
277             }
278             for (final String packageName : mInstalledPackages) {
279                 device.uninstallPackage(packageName);
280             }
281             if (!mDebugSkipAfterReboot) {
282                 reboot();
283             }
284         } catch (DeviceNotAvailableException e) {
285             Assert.fail(e.toString());
286         }
287     }
288 
289     /**
290      * A hacky workaround since {@link org.junit.AfterClass} and {@link ClassRule} require static
291      * members. Will defer assignment of the actual {@link TestRule} to execute until after any
292      * test case has been run.
293      *
294      * In effect, this makes the {@link ITestDevice} to be accessible after all test cases have
295      * been executed, allowing {@link ITestDevice#reboot()} to be used to fully restore the device.
296      */
297     public static class TestRuleDelegate implements TestRule {
298 
299         private boolean mThrowOnNull;
300 
301         @Nullable
302         private TestRule mTestRule;
303 
TestRuleDelegate(boolean throwOnNull)304         public TestRuleDelegate(boolean throwOnNull) {
305             mThrowOnNull = throwOnNull;
306         }
307 
setDelegate(TestRule testRule)308         public void setDelegate(TestRule testRule) {
309             mTestRule = testRule;
310         }
311 
312         @Override
apply(Statement base, Description description)313         public Statement apply(Statement base, Description description) {
314             if (mTestRule == null) {
315                 if (mThrowOnNull) {
316                     throw new IllegalStateException("TestRule delegate was not set");
317                 } else {
318                     return new Statement() {
319                         @Override
320                         public void evaluate() throws Throwable {
321                             base.evaluate();
322                         }
323                     };
324                 }
325             }
326 
327             Statement statement = mTestRule.apply(base, description);
328             mTestRule = null;
329             return statement;
330         }
331     }
332 
333     /**
334      * Forces a full reboot at the end of the test class to restore any device state.
335      */
336     private static class TearDownRule extends ExternalResource {
337 
338         private DeviceProvider mDeviceProvider;
339         private boolean mInitialized;
340         private boolean mWasVerityEnabled;
341         private boolean mWasAdbRoot;
342         private boolean mIsVerityEnabled;
343 
344         TearDownRule(DeviceProvider deviceProvider) {
345             mDeviceProvider = deviceProvider;
346         }
347 
348         @Override
349         protected void before() {
350             // This method will never be run
351         }
352 
353         @Override
354         protected void after() {
355             try {
356                 initialize();
357                 ITestDevice device = mDeviceProvider.getDevice();
358                 if (mWasVerityEnabled != mIsVerityEnabled) {
359                     device.executeShellCommand(
360                             mWasVerityEnabled ? "enable-verity" : "disable-verity");
361                 }
362                 device.reboot();
363                 if (!mWasAdbRoot) {
364                     device.disableAdbRoot();
365                 }
366             } catch (DeviceNotAvailableException e) {
367                 Assert.fail(e.toString());
368             }
369         }
370 
371         /**
372          * Remount is done inside this class so that the verity state can be tracked.
373          */
374         public void remount() throws DeviceNotAvailableException {
375             initialize();
376             ITestDevice device = mDeviceProvider.getDevice();
377             device.enableAdbRoot();
378             if (mIsVerityEnabled) {
379                 mIsVerityEnabled = false;
380                 device.executeShellCommand("disable-verity");
381                 device.reboot();
382             }
383             device.executeShellCommand("remount");
384             device.waitForDeviceAvailable();
385         }
386 
387         private void initialize() throws DeviceNotAvailableException {
388             if (mInitialized) {
389                 return;
390             }
391             mInitialized = true;
392             ITestDevice device = mDeviceProvider.getDevice();
393             mWasAdbRoot = device.isAdbRoot();
394             device.enableAdbRoot();
395             String veritySystem = device.getProperty("partition.system.verified");
396             String verityVendor = device.getProperty("partition.vendor.verified");
397             mWasVerityEnabled = (veritySystem != null && !veritySystem.isEmpty())
398                     || (verityVendor != null && !verityVendor.isEmpty());
399             mIsVerityEnabled = mWasVerityEnabled;
400         }
401     }
402 
403     public interface DeviceProvider {
404         ITestDevice getDevice();
405     }
406 
407     /**
408      * How to reboot the device. Ordered from slowest to fastest.
409      */
410     @SuppressWarnings("DanglingJavadoc")
411     public enum RebootStrategy {
412         /** @see ITestDevice#reboot() */
413         FULL,
414 
415         /** @see ITestDevice#rebootUntilOnline() () */
416         UNTIL_ONLINE,
417 
418         /** @see ITestDevice#rebootUserspace() */
419         USERSPACE,
420 
421         /** @see ITestDevice#rebootUserspaceUntilOnline() () */
422         USERSPACE_UNTIL_ONLINE,
423 
424         /**
425          * Uses shell stop && start to "reboot" the device. May leave invalid state after each test.
426          * Whether this matters or not depends on what's being tested.
427          *
428          * TODO(b/159540015): There's a bug with this causing unnecessary disk space usage, which
429          *  can eventually lead to an insufficient storage space error.
430          *
431          * This can be uncommented for local development, but should be left out when merging.
432          * It is done this way to hopefully be caught by code review, since merging this will
433          * break all of postsubmit. But the nearly 50% reduction in test runtime is worth having
434          * this option exist.
435          *
436          * @deprecated do not use this in merged code until bug is resolved
437          */
438 //        @Deprecated
439 //        START_STOP
440     }
441 }
442