• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 package com.android.tradefed.result.ddmlib;
17 
18 import com.android.annotations.NonNull;
19 import com.android.ddmlib.AdbCommandRejectedException;
20 import com.android.ddmlib.IDevice;
21 import com.android.ddmlib.IShellEnabledDevice;
22 import com.android.ddmlib.ShellCommandUnresponsiveException;
23 import com.android.ddmlib.TimeoutException;
24 import com.android.ddmlib.testrunner.IInstrumentationResultParser;
25 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
26 import com.android.ddmlib.testrunner.ITestRunListener;
27 import com.android.tradefed.log.Log;
28 import com.android.tradefed.log.LogUtil.CLog;
29 
30 import java.io.IOException;
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Hashtable;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.concurrent.TimeUnit;
37 import java.util.function.BiFunction;
38 
39 /** Runs a Android test command remotely and reports results. */
40 public class RemoteAndroidTestRunner implements IRemoteAndroidTestRunner {
41 
42     /** Represents a status reporter mode in am instrument command options. */
43     public enum StatusReporterMode {
44         /**
45          * Use raw text message to receive status from am instrument command.
46          *
47          * @deprecated Use {@link #PROTO_STD} for API level 26 and above.
48          */
49         @Deprecated
50         RAW_TEXT("-r", 0, InstrumentationResultParser::new),
51         /**
52          * Use instrumentationData protobuf status reporter to receive status from am instrument
53          * command.
54          */
55         PROTO_STD("-m", 26, InstrumentationProtoResultParser::new);
56 
StatusReporterMode( String amInstrumentCommandArg, int minApiLevel, BiFunction<String, Collection<ITestRunListener>, IInstrumentationResultParser> parserFactory)57         StatusReporterMode(
58                 String amInstrumentCommandArg,
59                 int minApiLevel,
60                 BiFunction<String, Collection<ITestRunListener>, IInstrumentationResultParser>
61                         parserFactory) {
62             this.amInstrumentCommandArg = amInstrumentCommandArg;
63             this.minApiLevel = minApiLevel;
64             this.parserFactory = parserFactory;
65         }
66 
67         private final String amInstrumentCommandArg;
68         private final int minApiLevel;
69         private final BiFunction<String, Collection<ITestRunListener>, IInstrumentationResultParser>
70                 parserFactory;
71 
72         /**
73          * Returns a command line arg for am instrument command to specify this status reporter
74          * mode.
75          */
getAmInstrumentCommandArg()76         public String getAmInstrumentCommandArg() {
77             return amInstrumentCommandArg;
78         }
79 
80         /**
81          * Returns the minimum Android API level which supports this instrumentation status report
82          * type.
83          */
getMinimumApiLevel()84         public int getMinimumApiLevel() {
85             return minApiLevel;
86         }
87 
88         /**
89          * Create the {@link InstrumentationResultParser} that can be used to parse the
90          * instrumentation output.
91          *
92          * @param runName The name of the run to use.
93          * @param listeners The listeners where to report the results.
94          * @return An instance of {@link InstrumentationResultParser}.
95          */
createInstrumentationResultParser( @onNull String runName, @NonNull Collection<ITestRunListener> listeners)96         public IInstrumentationResultParser createInstrumentationResultParser(
97                 @NonNull String runName, @NonNull Collection<ITestRunListener> listeners) {
98             return parserFactory.apply(runName, listeners);
99         }
100     }
101 
102     private final StatusReporterMode mStatusReporterMode;
103     private final String mPackageName;
104     private final String mRunnerName;
105     private IShellEnabledDevice mRemoteDevice;
106     // default to no timeout
107     private long mMaxTimeoutMs = 0L;
108     private long mMaxTimeToOutputResponseMs = 0L;
109     private String mRunName = null;
110 
111     /** map of name-value instrumentation argument pairs */
112     private Map<String, String> mArgMap;
113 
114     private IInstrumentationResultParser mParser;
115     private static final String LOG_TAG = "RemoteAndroidTest";
116     private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner";
117     private static final char CLASS_SEPARATOR = ',';
118     private static final char METHOD_SEPARATOR = '#';
119     private static final char RUNNER_SEPARATOR = '/';
120     // defined instrumentation argument names
121     private static final String CLASS_ARG_NAME = "class";
122     private static final String LOG_ARG_NAME = "log";
123     private static final String DEBUG_ARG_NAME = "debug";
124     private static final String COVERAGE_ARG_NAME = "coverage";
125     private static final String PACKAGE_ARG_NAME = "package";
126     private static final String SIZE_ARG_NAME = "size";
127     private static final String DELAY_MSEC_ARG_NAME = "delay_msec";
128     private String mRunOptions = "";
129     private static final int TEST_COLLECTION_TIMEOUT = 2 * 60 * 1000; // 2 min
130 
131     /**
132      * Creates a remote Android test runner.
133      *
134      * @param packageName the Android application package that contains the tests to run
135      * @param runnerName the instrumentation test runner to execute. If null, will use default
136      *     runner
137      * @param remoteDevice the Android device to execute tests on
138      * @param statusReporterMode the status reporter mode to be used for am instrument command
139      */
RemoteAndroidTestRunner( String packageName, String runnerName, IShellEnabledDevice remoteDevice, StatusReporterMode statusReporterMode)140     public RemoteAndroidTestRunner(
141             String packageName,
142             String runnerName,
143             IShellEnabledDevice remoteDevice,
144             StatusReporterMode statusReporterMode) {
145         mPackageName = packageName;
146         mRunnerName = runnerName;
147         mRemoteDevice = remoteDevice;
148         mStatusReporterMode = statusReporterMode;
149         mArgMap = new Hashtable<String, String>();
150     }
151 
152     /**
153      * Alternate constructor. Uses default {@code statusReporterMode}.
154      *
155      * @param packageName the Android application package that contains the tests to run
156      * @param runnerName the instrumentation test runner to execute. If null, will use default
157      *     runner
158      * @param remoteDevice the Android device to execute tests on
159      */
RemoteAndroidTestRunner( String packageName, String runnerName, IShellEnabledDevice remoteDevice)160     public RemoteAndroidTestRunner(
161             String packageName, String runnerName, IShellEnabledDevice remoteDevice) {
162         this(packageName, runnerName, remoteDevice, StatusReporterMode.RAW_TEXT);
163     }
164 
165     /**
166      * Alternate constructor. Uses default instrumentation runner.
167      *
168      * @param packageName the Android application package that contains the tests to run
169      * @param remoteDevice the Android device to execute tests on
170      */
RemoteAndroidTestRunner(String packageName, IShellEnabledDevice remoteDevice)171     public RemoteAndroidTestRunner(String packageName, IShellEnabledDevice remoteDevice) {
172         this(packageName, null, remoteDevice);
173     }
174 
175     @Override
getPackageName()176     public String getPackageName() {
177         return mPackageName;
178     }
179 
180     @Override
getRunnerName()181     public String getRunnerName() {
182         if (mRunnerName == null) {
183             return DEFAULT_RUNNER_NAME;
184         }
185         return mRunnerName;
186     }
187 
188     /** Returns the complete instrumentation component path. */
getRunnerPath()189     protected String getRunnerPath() {
190         return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
191     }
192 
193     @Override
setClassName(String className)194     public void setClassName(String className) {
195         // The class name may contain the dollar sign, so needs to be quoted.
196         addInstrumentationArg(CLASS_ARG_NAME, "'" + className + "'");
197     }
198 
199     @Override
setClassNames(String[] classNames)200     public void setClassNames(String[] classNames) {
201         StringBuilder classArgBuilder = new StringBuilder();
202         for (int i = 0; i < classNames.length; i++) {
203             if (i != 0) {
204                 classArgBuilder.append(CLASS_SEPARATOR);
205             }
206             classArgBuilder.append(classNames[i]);
207         }
208         setClassName(classArgBuilder.toString());
209     }
210 
211     @Override
setMethodName(String className, String testName)212     public void setMethodName(String className, String testName) {
213         setClassName(className + METHOD_SEPARATOR + testName);
214     }
215 
216     @Override
setTestPackageName(String packageName)217     public void setTestPackageName(String packageName) {
218         addInstrumentationArg(PACKAGE_ARG_NAME, packageName);
219     }
220 
221     @Override
addInstrumentationArg(String name, String value)222     public void addInstrumentationArg(String name, String value) {
223         if (name == null || value == null) {
224             throw new IllegalArgumentException("name or value arguments cannot be null");
225         }
226         mArgMap.put(name, value);
227     }
228 
229     @Override
removeInstrumentationArg(String name)230     public void removeInstrumentationArg(String name) {
231         if (name == null) {
232             throw new IllegalArgumentException("name argument cannot be null");
233         }
234         mArgMap.remove(name);
235     }
236 
237     @Override
addBooleanArg(String name, boolean value)238     public void addBooleanArg(String name, boolean value) {
239         addInstrumentationArg(name, Boolean.toString(value));
240     }
241 
242     @Override
setLogOnly(boolean logOnly)243     public void setLogOnly(boolean logOnly) {
244         addBooleanArg(LOG_ARG_NAME, logOnly);
245     }
246 
247     @Override
setDebug(boolean debug)248     public void setDebug(boolean debug) {
249         addBooleanArg(DEBUG_ARG_NAME, debug);
250     }
251 
252     @Override
setAdditionalTestOutputLocation(String additionalTestDataPath)253     public void setAdditionalTestOutputLocation(String additionalTestDataPath) {
254         addInstrumentationArg("additionalTestOutputDir", additionalTestDataPath);
255     }
256 
257     @Override
setCoverage(boolean coverage)258     public void setCoverage(boolean coverage) {
259         addBooleanArg(COVERAGE_ARG_NAME, coverage);
260     }
261 
262     @Override
setCoverageReportLocation(String reportPath)263     public void setCoverageReportLocation(String reportPath) {
264         addInstrumentationArg("coverageFile", reportPath);
265     }
266 
267     @Override
getCoverageOutputType()268     public CoverageOutput getCoverageOutputType() {
269         return CoverageOutput.FILE;
270     }
271 
272     @Override
setTestSize(TestSize size)273     public void setTestSize(TestSize size) {
274         addInstrumentationArg(SIZE_ARG_NAME, "small");
275     }
276 
277     @Override
setTestCollection(boolean collect)278     public void setTestCollection(boolean collect) {
279         if (collect) {
280             // skip test execution
281             setLogOnly(true);
282             // force a timeout for test collection
283             setMaxTimeToOutputResponse(TEST_COLLECTION_TIMEOUT, TimeUnit.MILLISECONDS);
284             if (getApiLevel() < 16) {
285                 // On older platforms, collecting tests can fail for large volume of tests.
286                 // Insert a small delay between each test to prevent this
287                 addInstrumentationArg(DELAY_MSEC_ARG_NAME, "15" /* ms */);
288             }
289         } else {
290             setLogOnly(false);
291             // restore timeout to its original set value
292             setMaxTimeToOutputResponse(mMaxTimeToOutputResponseMs, TimeUnit.MILLISECONDS);
293             if (getApiLevel() < 16) {
294                 // remove delay
295                 removeInstrumentationArg(DELAY_MSEC_ARG_NAME);
296             }
297         }
298     }
299 
300     /**
301      * Attempts to retrieve the Api level of the Android device
302      *
303      * @return the api level or -1 if the communication with the device wasn't successful
304      */
getApiLevel()305     private int getApiLevel() {
306         try {
307             return Integer.parseInt(
308                     mRemoteDevice.getSystemProperty(IDevice.PROP_BUILD_API_LEVEL).get());
309         } catch (Exception e) {
310             return -1;
311         }
312     }
313 
314     @Deprecated
315     @Override
setMaxtimeToOutputResponse(int maxTimeToOutputResponse)316     public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) {
317         setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS);
318     }
319 
320     @Override
setMaxTimeToOutputResponse(long maxTimeToOutputResponse, TimeUnit maxTimeUnits)321     public void setMaxTimeToOutputResponse(long maxTimeToOutputResponse, TimeUnit maxTimeUnits) {
322         mMaxTimeToOutputResponseMs = maxTimeUnits.toMillis(maxTimeToOutputResponse);
323     }
324 
325     @Override
setMaxTimeout(long maxTimeout, TimeUnit maxTimeUnits)326     public void setMaxTimeout(long maxTimeout, TimeUnit maxTimeUnits) {
327         mMaxTimeoutMs = maxTimeUnits.toMillis(maxTimeout);
328     }
329 
330     @Override
setRunName(String runName)331     public void setRunName(String runName) {
332         mRunName = runName;
333     }
334 
335     @Override
run(ITestRunListener... listeners)336     public void run(ITestRunListener... listeners)
337             throws TimeoutException,
338                     AdbCommandRejectedException,
339                     ShellCommandUnresponsiveException,
340                     IOException {
341         run(Arrays.asList(listeners));
342     }
343 
344     @Override
run(Collection<ITestRunListener> listeners)345     public void run(Collection<ITestRunListener> listeners)
346             throws TimeoutException,
347                     AdbCommandRejectedException,
348                     ShellCommandUnresponsiveException,
349                     IOException {
350         final String runCaseCommandStr = getAmInstrumentCommand();
351         CLog.i("Running %1$s on %2$s", runCaseCommandStr, mRemoteDevice.getName());
352         String runName = mRunName == null ? mPackageName : mRunName;
353         mParser = createParser(runName, listeners);
354         try {
355             mRemoteDevice.executeShellCommand(
356                     runCaseCommandStr,
357                     mParser,
358                     mMaxTimeoutMs,
359                     mMaxTimeToOutputResponseMs,
360                     TimeUnit.MILLISECONDS);
361         } catch (IOException e) {
362             Log.w(
363                     LOG_TAG,
364                     String.format(
365                             "IOException %1$s when running tests %2$s on %3$s",
366                             e.toString(), getPackageName(), mRemoteDevice.getName()));
367             // rely on parser to communicate results to listeners
368             mParser.handleTestRunFailed(e.toString());
369             throw e;
370         } catch (ShellCommandUnresponsiveException e) {
371             Log.w(
372                     LOG_TAG,
373                     String.format(
374                             "ShellCommandUnresponsiveException %1$s when running tests %2$s on"
375                                     + " %3$s",
376                             e.toString(), getPackageName(), mRemoteDevice.getName()));
377             mParser.handleTestRunFailed(
378                     String.format(
379                             "Failed to receive adb shell test output within %1$d ms. Test may have "
380                                     + "timed out, or adb connection to device became unresponsive",
381                             mMaxTimeToOutputResponseMs));
382             throw e;
383         } catch (TimeoutException e) {
384             Log.w(
385                     LOG_TAG,
386                     String.format(
387                             "TimeoutException when running tests %1$s on %2$s",
388                             getPackageName(), mRemoteDevice.getName()));
389             mParser.handleTestRunFailed(e.toString());
390             throw e;
391         } catch (AdbCommandRejectedException e) {
392             Log.w(
393                     LOG_TAG,
394                     String.format(
395                             "AdbCommandRejectedException %1$s when running tests %2$s on %3$s",
396                             e.toString(), getPackageName(), mRemoteDevice.getName()));
397             mParser.handleTestRunFailed(e.toString());
398             throw e;
399         }
400     }
401 
402     /**
403      * Create the {@link InstrumentationResultParser} that will be used to parse the instrumentation
404      * output.
405      *
406      * @param runName The name of the run to use.
407      * @param listeners The listeners where to report the results.
408      * @return An instance of {@link InstrumentationResultParser}.
409      */
410     @NonNull
createParser( @onNull String runName, @NonNull Collection<ITestRunListener> listeners)411     public IInstrumentationResultParser createParser(
412             @NonNull String runName, @NonNull Collection<ITestRunListener> listeners) {
413         return mStatusReporterMode.createInstrumentationResultParser(runName, listeners);
414     }
415 
416     @NonNull
getAmInstrumentCommand()417     public String getAmInstrumentCommand() {
418         return String.format(
419                 "am instrument -w %1$s %2$s %3$s %4$s",
420                 mStatusReporterMode.getAmInstrumentCommandArg(),
421                 getRunOptions(),
422                 getArgsCommand(),
423                 getRunnerPath());
424     }
425 
426     /** Returns options for the am instrument command. */
427     @NonNull
getRunOptions()428     public String getRunOptions() {
429         return mRunOptions;
430     }
431 
432     /**
433      * Sets options for the am instrument command. See com/android/commands/am/Am.java for full list
434      * of options.
435      */
setRunOptions(@onNull String options)436     public void setRunOptions(@NonNull String options) {
437         mRunOptions = options;
438     }
439 
440     @Override
cancel()441     public void cancel() {
442         if (mParser != null) {
443             mParser.cancel();
444         }
445     }
446 
447     /**
448      * Returns the full instrumentation command line syntax for the provided instrumentation
449      * arguments. Returns an empty string if no arguments were specified.
450      */
getArgsCommand()451     protected String getArgsCommand() {
452         StringBuilder commandBuilder = new StringBuilder();
453         for (Entry<String, String> argPair : mArgMap.entrySet()) {
454             final String argCmd =
455                     String.format(" -e %1$s %2$s", argPair.getKey(), argPair.getValue());
456             commandBuilder.append(argCmd);
457         }
458         return commandBuilder.toString();
459     }
460 }
461