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.compatibility.targetprep;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 
21 import com.android.csuite.core.SystemPackageUninstaller;
22 import com.android.tradefed.config.ConfigurationException;
23 import com.android.tradefed.config.Option;
24 import com.android.tradefed.config.OptionSetter;
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.device.ITestDevice;
27 import com.android.tradefed.invoker.TestInformation;
28 import com.android.tradefed.log.ITestLogger;
29 import com.android.tradefed.log.LogUtil.CLog;
30 import com.android.tradefed.result.FileInputStreamSource;
31 import com.android.tradefed.result.ITestLoggerReceiver;
32 import com.android.tradefed.result.LogDataType;
33 import com.android.tradefed.targetprep.BuildError;
34 import com.android.tradefed.targetprep.ITargetPreparer;
35 import com.android.tradefed.targetprep.TargetSetupError;
36 import com.android.tradefed.targetprep.TestAppInstallSetup;
37 import com.android.tradefed.util.AaptParser.AaptVersion;
38 import com.android.tradefed.util.ZipUtil;
39 
40 import com.google.common.annotations.VisibleForTesting;
41 import com.google.common.util.concurrent.SimpleTimeLimiter;
42 import com.google.common.util.concurrent.TimeLimiter;
43 import com.google.common.util.concurrent.UncheckedTimeoutException;
44 
45 import java.io.File;
46 import java.io.IOException;
47 import java.time.Duration;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.concurrent.Executors;
51 import java.util.concurrent.TimeUnit;
52 
53 /** A Tradefed preparer that downloads and installs an app on the target device. */
54 public final class AppSetupPreparer implements ITargetPreparer, ITestLoggerReceiver {
55 
56     @VisibleForTesting
57     static final String OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS =
58             "wait-for-device-available-seconds";
59 
60     @VisibleForTesting
61     static final String OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS =
62             "exponential-backoff-multiplier-seconds";
63 
64     @VisibleForTesting static final String OPTION_TEST_FILE_NAME = "test-file-name";
65     @VisibleForTesting static final String OPTION_INSTALL_ARG = "install-arg";
66     @VisibleForTesting static final String OPTION_SETUP_TIMEOUT_MILLIS = "setup-timeout-millis";
67     @VisibleForTesting static final String OPTION_MAX_RETRY = "max-retry";
68     @VisibleForTesting static final String OPTION_AAPT_VERSION = "aapt-version";
69     @VisibleForTesting static final String OPTION_INCREMENTAL_INSTALL = "incremental";
70     @VisibleForTesting static final String OPTION_INCREMENTAL_FILTER = "incremental-block-filter";
71     @VisibleForTesting static final String OPTION_SAVE_APKS = "save-apks";
72 
73     @VisibleForTesting
74     static final String OPTION_INCREMENTAL_TIMEOUT_SECS = "incremental-install-timeout-secs";
75 
76     @Option(name = "package-name", description = "Package name of testing app.")
77     private String mPackageName;
78 
79     @Option(
80             name = OPTION_TEST_FILE_NAME,
81             description = "the name of an apk file to be installed on device. Can be repeated.")
82     private final List<File> mTestFiles = new ArrayList<>();
83 
84     @Option(name = OPTION_AAPT_VERSION, description = "The version of AAPT for APK parsing.")
85     private AaptVersion mAaptVersion = AaptVersion.AAPT2;
86 
87     @Option(
88             name = OPTION_INSTALL_ARG,
89             description =
90                     "Additional arguments to be passed to install command, "
91                             + "including leading dash, e.g. \"-d\"")
92     private final List<String> mInstallArgs = new ArrayList<>();
93 
94     @Option(
95             name = OPTION_INCREMENTAL_INSTALL,
96             description = "Enable packages to be installed incrementally.")
97     private boolean mIncrementalInstallation = false;
98 
99     @Option(
100             name = OPTION_INCREMENTAL_FILTER,
101             description = "Specify percentage of blocks to filter.")
102     private double mBlockFilterPercentage = 0.0;
103 
104     @Option(
105             name = OPTION_INCREMENTAL_TIMEOUT_SECS,
106             description = "Specify timeout of incremental installation.")
107     private int mIncrementalTimeout = 1800;
108 
109     @Option(name = OPTION_MAX_RETRY, description = "Max number of retries upon TargetSetupError.")
110     private int mMaxRetry = 0;
111 
112     @Option(
113             name = OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS,
114             description =
115                     "The exponential backoff multiplier for retries in seconds. "
116                             + "A value n means the preparer will wait for n^(retry_count) "
117                             + "seconds between retries.")
118     private int mExponentialBackoffMultiplierSeconds = 0;
119 
120     @Option(
121             name = OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS,
122             description =
123                     "Timeout value for waiting for device available in seconds. "
124                             + "A negative value means not to check device availability.")
125     private int mWaitForDeviceAvailableSeconds = -1;
126 
127     @Option(
128             name = OPTION_SETUP_TIMEOUT_MILLIS,
129             description =
130                     "Timeout value for a setUp operation. "
131                             + "Note that the timeout is not a global timeout and will "
132                             + "be applied to each retry attempt.")
133     private long mSetupOnceTimeoutMillis = TimeUnit.MINUTES.toMillis(10);
134 
135     @Option(
136             name = OPTION_SAVE_APKS,
137             description = "Whether to save the input APKs into test output.")
138     private boolean mSaveApks = false;
139 
140     private final TestAppInstallSetup mTestAppInstallSetup;
141     private final Sleeper mSleeper;
142     private final TimeLimiter mTimeLimiter =
143             SimpleTimeLimiter.create(Executors.newCachedThreadPool());
144     private ITestLogger mTestLogger;
145 
AppSetupPreparer()146     public AppSetupPreparer() {
147         this(new TestAppInstallSetup(), Sleepers.DefaultSleeper.INSTANCE);
148     }
149 
150     @VisibleForTesting
AppSetupPreparer(TestAppInstallSetup testAppInstallSetup, Sleeper sleeper)151     public AppSetupPreparer(TestAppInstallSetup testAppInstallSetup, Sleeper sleeper) {
152         mTestAppInstallSetup = testAppInstallSetup;
153         mSleeper = sleeper;
154     }
155 
156     /** {@inheritDoc} */
157     @Override
setUp(TestInformation testInfo)158     public void setUp(TestInformation testInfo)
159             throws DeviceNotAvailableException, BuildError, TargetSetupError {
160         checkArgumentNonNegative(mMaxRetry, OPTION_MAX_RETRY);
161         checkArgumentNonNegative(
162                 mExponentialBackoffMultiplierSeconds,
163                 OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS);
164         checkArgumentNonNegative(mSetupOnceTimeoutMillis, OPTION_SETUP_TIMEOUT_MILLIS);
165 
166         if (mSaveApks) {
167             mTestFiles.forEach(
168                     path -> {
169                         if (!path.exists()) {
170                             CLog.w(
171                                     "Skipping saving %s as the path might be a relative path.",
172                                     path);
173                             return;
174                         }
175                         try {
176                             File outputZip = ZipUtil.createZip(path);
177                             mTestLogger.testLog(
178                                     mPackageName + "-input_apk-" + path.getName(),
179                                     LogDataType.ZIP,
180                                     new FileInputStreamSource(outputZip));
181                         } catch (IOException e) {
182                             CLog.e("Failed to zip the output directory: " + e);
183                         }
184                     });
185         }
186 
187         int runCount = 0;
188         while (true) {
189             TargetSetupError currentException;
190             try {
191                 runCount++;
192 
193                 ITargetPreparer handler =
194                         mTimeLimiter.newProxy(
195                                 new ITargetPreparer() {
196                                     @Override
197                                     public void setUp(TestInformation testInfo)
198                                             throws DeviceNotAvailableException, BuildError,
199                                                     TargetSetupError {
200                                         setUpOnce(testInfo);
201                                     }
202                                 },
203                                 ITargetPreparer.class,
204                                 mSetupOnceTimeoutMillis,
205                                 TimeUnit.MILLISECONDS);
206                 handler.setUp(testInfo);
207 
208                 break;
209             } catch (TargetSetupError e) {
210                 currentException = e;
211             } catch (UncheckedTimeoutException e) {
212                 currentException =
213                         new TargetSetupError(
214                                 e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor());
215             }
216 
217             waitForDeviceAvailable(testInfo.getDevice());
218             if (runCount > mMaxRetry) {
219                 throw currentException;
220             }
221             CLog.w("setUp failed: %s. Run count: %d. Retrying...", currentException, runCount);
222 
223             try {
224                 mSleeper.sleep(
225                         Duration.ofSeconds(
226                                 (int) Math.pow(mExponentialBackoffMultiplierSeconds, runCount)));
227             } catch (InterruptedException e) {
228                 Thread.currentThread().interrupt();
229                 throw new TargetSetupError(
230                         e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor());
231             }
232         }
233     }
234 
setUpOnce(TestInformation testInfo)235     private void setUpOnce(TestInformation testInfo)
236             throws DeviceNotAvailableException, BuildError, TargetSetupError {
237         mTestAppInstallSetup.setAaptVersion(mAaptVersion);
238 
239         try {
240             OptionSetter setter = new OptionSetter(mTestAppInstallSetup);
241             setter.setOptionValue("incremental", String.valueOf(mIncrementalInstallation));
242             setter.setOptionValue(
243                     "incremental-block-filter", String.valueOf(mBlockFilterPercentage));
244             setter.setOptionValue(
245                     "incremental-install-timeout-secs", String.valueOf(mIncrementalTimeout));
246         } catch (ConfigurationException e) {
247             throw new TargetSetupError(
248                     e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor());
249         }
250 
251         if (mPackageName != null) {
252             SystemPackageUninstaller.uninstallPackage(mPackageName, testInfo.getDevice());
253         }
254 
255         for (File testFile : mTestFiles) {
256             CLog.d("Adding apk path %s for installation.", testFile);
257             mTestAppInstallSetup.addTestFile(testFile);
258         }
259 
260         for (String installArg : mInstallArgs) {
261             mTestAppInstallSetup.addInstallArg(installArg);
262         }
263 
264         mTestAppInstallSetup.setUp(testInfo);
265     }
266 
267     /** {@inheritDoc} */
268     @Override
tearDown(TestInformation testInfo, Throwable e)269     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
270         mTestAppInstallSetup.tearDown(testInfo, e);
271     }
272 
waitForDeviceAvailable(ITestDevice device)273     private void waitForDeviceAvailable(ITestDevice device) throws DeviceNotAvailableException {
274         if (mWaitForDeviceAvailableSeconds < 0) {
275             return;
276         }
277 
278         device.waitForDeviceAvailable(1000L * mWaitForDeviceAvailableSeconds);
279     }
280 
checkArgumentNonNegative(long val, String name)281     private void checkArgumentNonNegative(long val, String name) {
282         checkArgument(val >= 0, "%s (%s) must not be negative", name, val);
283     }
284 
285     @VisibleForTesting
286     interface Sleeper {
sleep(Duration duration)287         void sleep(Duration duration) throws InterruptedException;
288     }
289 
290     private static class Sleepers {
291         enum DefaultSleeper implements Sleeper {
292             INSTANCE;
293 
294             @Override
sleep(Duration duration)295             public void sleep(Duration duration) throws InterruptedException {
296                 Thread.sleep(duration.toMillis());
297             }
298         }
299 
Sleepers()300         private Sleepers() {}
301     }
302 
303     @Override
setTestLogger(ITestLogger testLogger)304     public void setTestLogger(ITestLogger testLogger) {
305         mTestLogger = testLogger;
306     }
307 }
308