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