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