1 /* 2 * Copyright (C) 2023 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.webview.tests; 18 19 import com.android.csuite.core.ApkInstaller; 20 import com.android.csuite.core.ApkInstaller.ApkInstallerException; 21 import com.android.csuite.core.AppCrawlTester; 22 import com.android.csuite.core.DeviceUtils; 23 import com.android.csuite.core.TestUtils; 24 import com.android.tradefed.config.Option; 25 import com.android.tradefed.device.DeviceNotAvailableException; 26 import com.android.tradefed.log.LogUtil.CLog; 27 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 28 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; 29 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 30 31 import com.google.common.base.Preconditions; 32 33 import org.junit.After; 34 import org.junit.Assert; 35 import org.junit.Before; 36 import org.junit.Rule; 37 import org.junit.Test; 38 import org.junit.runner.RunWith; 39 40 import java.io.File; 41 import java.io.IOException; 42 import java.nio.file.Path; 43 import java.util.ArrayList; 44 import java.util.List; 45 46 import javax.annotation.Nullable; 47 48 /** A test that verifies that a single app can be successfully launched. */ 49 @RunWith(DeviceJUnit4ClassRunner.class) 50 public class WebviewAppCrawlTest extends BaseHostJUnit4Test { 51 @Rule public TestLogData mLogData = new TestLogData(); 52 53 private static final String COLLECT_APP_VERSION = "collect-app-version"; 54 private static final String COLLECT_GMS_VERSION = "collect-gms-version"; 55 private static final long COMMAND_TIMEOUT_MILLIS = 5 * 60 * 1000; 56 57 private WebviewUtils mWebviewUtils; 58 private WebviewPackage mPreInstalledWebview; 59 private ApkInstaller mApkInstaller; 60 private AppCrawlTester mCrawler; 61 62 @Option(name = "record-screen", description = "Whether to record screen during test.") 63 private boolean mRecordScreen; 64 65 @Option(name = "webview-version-to-test", description = "Version of Webview to test.") 66 private String mWebviewVersionToTest; 67 68 @Option( 69 name = "release-channel", 70 description = "Release channel to fetch Webview from, i.e. stable.") 71 private String mReleaseChannel; 72 73 @Option(name = "package-name", description = "Package name of testing app.") 74 private String mPackageName; 75 76 @Option( 77 name = "install-apk", 78 description = 79 "The path to an apk file or a directory of apk files of a singe package to be" 80 + " installed on device. Can be repeated.") 81 private List<File> mApkPaths = new ArrayList<>(); 82 83 @Option( 84 name = "install-arg", 85 description = "Arguments for the 'adb install-multiple' package installation command.") 86 private final List<String> mInstallArgs = new ArrayList<>(); 87 88 @Option( 89 name = "app-launch-timeout-ms", 90 description = "Time to wait for an app to launch in msecs.") 91 private int mAppLaunchTimeoutMs = 20000; 92 93 @Option( 94 name = COLLECT_APP_VERSION, 95 description = 96 "Whether to collect package version information and store the information in" 97 + " test log files.") 98 private boolean mCollectAppVersion; 99 100 @Option( 101 name = COLLECT_GMS_VERSION, 102 description = 103 "Whether to collect GMS core version information and store the information in" 104 + " test log files.") 105 private boolean mCollectGmsVersion; 106 107 @Option( 108 name = "repack-apk", 109 mandatory = false, 110 description = 111 "Path to an apk file or a directory containing apk files of a single package " 112 + "to repack and install in Espresso mode") 113 private File mRepackApk; 114 115 @Option( 116 name = "crawl-controller-endpoint", 117 mandatory = false, 118 description = "The crawl controller endpoint to target.") 119 private String mCrawlControllerEndpoint; 120 121 @Option( 122 name = "ui-automator-mode", 123 mandatory = false, 124 description = 125 "Run the crawler with UIAutomator mode. Apk option is not required in this" 126 + " mode.") 127 private boolean mUiAutomatorMode = false; 128 129 @Option( 130 name = "robo-script-file", 131 description = "A Roboscript file to be executed by the crawler.") 132 private File mRoboscriptFile; 133 134 // TODO(b/234512223): add support for contextual roboscript files 135 136 @Option( 137 name = "crawl-guidance-proto-file", 138 description = "A CrawlGuidance file to be executed by the crawler.") 139 private File mCrawlGuidanceProtoFile; 140 141 @Option( 142 name = "timeout-sec", 143 mandatory = false, 144 description = "The timeout for the crawl test.") 145 private int mTimeoutSec = 60; 146 147 @Option( 148 name = "save-apk-when", 149 description = "When to save apk files to the test result artifacts.") 150 private TestUtils.TakeEffectWhen mSaveApkWhen = TestUtils.TakeEffectWhen.NEVER; 151 152 @Option( 153 name = "login-config-dir", 154 description = 155 "A directory containing Roboscript and CrawlGuidance files with login" 156 + " credentials that are passed to the crawler. There should be one config" 157 + " file per package name. If both Roboscript and CrawlGuidance files are" 158 + " present, only the Roboscript file will be used.") 159 private File mLoginConfigDir; 160 161 @Before setUp()162 public void setUp() throws DeviceNotAvailableException, ApkInstallerException, IOException { 163 Assert.assertNotNull("Package name cannot be null", mPackageName); 164 Assert.assertTrue( 165 "Either the --release-channel or --webview-version-to-test arguments " 166 + "must be used", 167 mWebviewVersionToTest != null || mReleaseChannel != null); 168 169 mCrawler = AppCrawlTester.newInstance(mPackageName, getTestInformation(), mLogData); 170 if (!mUiAutomatorMode) { 171 setApkForEspressoMode(); 172 } 173 mCrawler.setCrawlControllerEndpoint(mCrawlControllerEndpoint); 174 mCrawler.setRecordScreen(mRecordScreen); 175 mCrawler.setCollectGmsVersion(mCollectGmsVersion); 176 mCrawler.setCollectAppVersion(mCollectAppVersion); 177 mCrawler.setUiAutomatorMode(mUiAutomatorMode); 178 mCrawler.setRoboscriptFile(toPathOrNull(mRoboscriptFile)); 179 mCrawler.setCrawlGuidanceProtoFile(toPathOrNull(mCrawlGuidanceProtoFile)); 180 mCrawler.setLoginConfigDir(toPathOrNull(mLoginConfigDir)); 181 mCrawler.setTimeoutSec(mTimeoutSec); 182 183 mApkInstaller = ApkInstaller.getInstance(getDevice()); 184 mWebviewUtils = new WebviewUtils(getTestInformation()); 185 mPreInstalledWebview = mWebviewUtils.getCurrentWebviewPackage(); 186 187 for (File apkPath : mApkPaths) { 188 CLog.d("Installing " + apkPath); 189 mApkInstaller.install(apkPath.toPath(), mInstallArgs); 190 } 191 192 DeviceUtils.getInstance(getDevice()).freezeRotation(); 193 mWebviewUtils.printWebviewVersion(); 194 } 195 196 /** 197 * For Espresso mode, checks that a path with the location of the apk to repackage was provided 198 */ setApkForEspressoMode()199 private void setApkForEspressoMode() { 200 Preconditions.checkNotNull( 201 mRepackApk, "Apk file path is required when not running in UIAutomator mode"); 202 // set the root path of the target apk for Espresso mode 203 mCrawler.setApkPath(mRepackApk.toPath()); 204 } 205 toPathOrNull(@ullable File f)206 private static Path toPathOrNull(@Nullable File f) { 207 return f == null ? null : f.toPath(); 208 } 209 210 @Test testAppCrawl()211 public void testAppCrawl() 212 throws DeviceNotAvailableException, InterruptedException, ApkInstallerException, 213 IOException { 214 AssertionError lastError = null; 215 WebviewPackage lastWebviewInstalled = 216 mWebviewUtils.installWebview(mWebviewVersionToTest, mReleaseChannel); 217 218 try { 219 mCrawler.startAndAssertAppNoCrash(); 220 } catch (AssertionError e) { 221 lastError = e; 222 } finally { 223 mWebviewUtils.uninstallWebview(lastWebviewInstalled, mPreInstalledWebview); 224 } 225 226 // If the app doesn't crash, complete the test. 227 if (lastError == null) { 228 return; 229 } 230 231 // If the app crashes, try the app with the original webview version that comes with the 232 // device. 233 try { 234 mCrawler.startAndAssertAppNoCrash(); 235 } catch (AssertionError newError) { 236 CLog.w( 237 "The app %s crashed both with and without the webview installation," 238 + " ignoring the failure...", 239 mPackageName); 240 return; 241 } 242 throw new AssertionError( 243 String.format( 244 "Package %s crashed since webview version %s", 245 mPackageName, lastWebviewInstalled.getVersion()), 246 lastError); 247 } 248 249 @After tearDown()250 public void tearDown() throws DeviceNotAvailableException, ApkInstallerException { 251 TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); 252 testUtils.collectScreenshot(mPackageName); 253 254 DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); 255 deviceUtils.stopPackage(mPackageName); 256 deviceUtils.unfreezeRotation(); 257 258 mApkInstaller.uninstallAllInstalledPackages(); 259 mWebviewUtils.printWebviewVersion(); 260 261 if (!mUiAutomatorMode) { 262 getDevice().uninstallPackage(mPackageName); 263 } 264 265 mCrawler.cleanUp(); 266 } 267 } 268