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