• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.testtype;
17 
18 import com.android.tradefed.build.IBuildInfo;
19 import com.android.tradefed.build.IFolderBuildInfo;
20 import com.android.tradefed.command.CommandOptions;
21 import com.android.tradefed.config.GlobalConfiguration;
22 import com.android.tradefed.config.IConfiguration;
23 import com.android.tradefed.config.IConfigurationReceiver;
24 import com.android.tradefed.config.Option;
25 import com.android.tradefed.config.proxy.AutomatedReporters;
26 import com.android.tradefed.device.DeviceNotAvailableException;
27 import com.android.tradefed.error.HarnessRuntimeException;
28 import com.android.tradefed.invoker.DelegatedInvocationExecution;
29 import com.android.tradefed.invoker.IInvocationContext;
30 import com.android.tradefed.invoker.RemoteInvocationExecution;
31 import com.android.tradefed.invoker.TestInformation;
32 import com.android.tradefed.log.LogUtil.CLog;
33 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
34 import com.android.tradefed.result.FailureDescription;
35 import com.android.tradefed.result.FileInputStreamSource;
36 import com.android.tradefed.result.ITestInvocationListener;
37 import com.android.tradefed.result.LogDataType;
38 import com.android.tradefed.result.TestDescription;
39 import com.android.tradefed.result.error.InfraErrorIdentifier;
40 import com.android.tradefed.result.proto.StreamProtoReceiver;
41 import com.android.tradefed.result.proto.StreamProtoResultReporter;
42 import com.android.tradefed.service.TradefedFeatureServer;
43 import com.android.tradefed.util.CommandResult;
44 import com.android.tradefed.util.CommandStatus;
45 import com.android.tradefed.util.FileUtil;
46 import com.android.tradefed.util.IRunUtil;
47 import com.android.tradefed.util.IRunUtil.EnvPriority;
48 import com.android.tradefed.util.RunUtil;
49 import com.android.tradefed.util.StreamUtil;
50 import com.android.tradefed.util.StringEscapeUtils;
51 import com.android.tradefed.util.SubprocessExceptionParser;
52 import com.android.tradefed.util.SubprocessTestResultsParser;
53 import com.android.tradefed.util.SystemUtil;
54 import com.android.tradefed.util.TimeUtil;
55 import com.android.tradefed.util.UniqueMultiMap;
56 
57 import org.junit.Assert;
58 
59 import java.io.File;
60 import java.io.FileOutputStream;
61 import java.io.IOException;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.HashMap;
65 import java.util.LinkedHashSet;
66 import java.util.List;
67 import java.util.Set;
68 
69 /**
70  * A {@link IRemoteTest} for running tests against a separate TF installation.
71  *
72  * <p>Launches an external java process to run the tests. Used for running the TF unit or functional
73  * tests continuously.
74  */
75 public abstract class SubprocessTfLauncher
76         implements IBuildReceiver, IInvocationContextReceiver, IRemoteTest, IConfigurationReceiver {
77 
78     /** The tag that will be passed to the TF subprocess to differentiate it */
79     public static final String SUBPROCESS_TAG_NAME = "subprocess";
80 
81     public static final String PARENT_PROC_TAG_NAME = "parentprocess";
82     /** Env. variable that affects adb selection. */
83     public static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL";
84 
85     @Option(name = "max-run-time", description =
86             "The maximum time to allow for a TF test run.", isTimeVal = true)
87     private long mMaxTfRunTime = 20 * 60 * 1000;
88 
89     @Option(name = "remote-debug", description =
90             "Start the TF java process in remote debug mode.")
91     private boolean mRemoteDebug = false;
92 
93     @Option(name = "config-name", description = "The config that runs the TF tests")
94     private String mConfigName;
95 
96     @Option(
97             name = "local-sharding-mode",
98             description =
99                     "If sharding is requested, allow the launcher to run with local sharding.")
100     private boolean mLocalShardingMode = false;
101 
102     @Option(name = "use-event-streaming", description = "Use a socket to receive results as they"
103             + "arrived instead of using a temporary file and parsing at the end.")
104     private boolean mEventStreaming = true;
105 
106     @Option(
107             name = "use-proto-reporting",
108             description = "Use a proto result reporter for the results from the subprocess.")
109     private boolean mUseProtoReporting = true;
110 
111     @Option(name = "sub-global-config", description = "The global config name to pass to the"
112             + "sub process, can be local or from jar resources. Be careful of conflicts with "
113             + "parent process.")
114     private String mGlobalConfig = null;
115 
116     @Option(
117             name = "inject-invocation-data",
118             description = "Pass the invocation-data to the subprocess if enabled.")
119     private boolean mInjectInvocationData = true;
120 
121     @Option(name = "ignore-test-log", description = "Only rely on logAssociation for logs.")
122     private boolean mIgnoreTestLog = true;
123 
124     @Option(
125         name = "disable-stderr-test",
126         description = "Whether or not to disable the stderr validation check."
127     )
128     private boolean mDisableStderrTest = false;
129 
130     @Option(
131         name = "disable-add-opens",
132         description = "Whether or not to add the java add-opens flags"
133     )
134     private boolean mDisableJavaOpens = false;
135 
136     @Option(name = "add-opens", description = "Whether or not to add the java add-opens flags")
137     private Set<String> mAddOpens =
138             new LinkedHashSet<>(
139                     Arrays.asList(
140                             "java.base/java.nio",
141                             "java.base/sun.reflect.annotation",
142                             "java.base/java.io"));
143 
144     // Represents all the args to be passed to the sub process
145     @Option(name = "sub-params", description = "Parameters to feed the subprocess.")
146     private List<String> mSubParams = new ArrayList<String>();
147 
148     // Temp global configuration filtered from the parent process.
149     private String mFilteredGlobalConfig = null;
150 
151     private static final List<String> TRADEFED_JARS =
152             new ArrayList<>(
153                     Arrays.asList(
154                             // Loganalysis
155                             "loganalysis.jar",
156                             "loganalysis-tests.jar",
157                             // Aosp Tf jars
158                             "tradefed.jar",
159                             "tradefed-tests.jar",
160                             // AVD util test jar
161                             "^tradefed-avd-util-tests.jar",
162                             // libs
163                             "tools-common-prebuilt.jar",
164                             // jar in older branches
165                             "tf-prod-tests.jar",
166                             "tf-prod-metatests.jar",
167                             // Aosp contrib jars
168                             "tradefed-contrib.jar",
169                             "tf-contrib-tests.jar",
170                             // Google Tf jars
171                             "google-tf-prod-tests.jar",
172                             "google-tf-prod-metatests.jar",
173                             "google-tradefed.jar",
174                             "google-tradefed-tests.jar",
175                             // Google contrib jars
176                             "google-tradefed-contrib.jar",
177                             // Older jar required for coverage tests
178                             "jack-jacoco-reporter.jar",
179                             "emmalib.jar"));
180 
181     /** Timeout to wait for the events received from subprocess to finish being processed.*/
182     private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
183 
184     protected IRunUtil mRunUtil =  new RunUtil();
185 
186     protected IBuildInfo mBuildInfo = null;
187     // Temp directory to run the TF process.
188     protected File mTmpDir = null;
189     // List of command line arguments to run the TF process.
190     protected List<String> mCmdArgs = null;
191     // The absolute path to the build's root directory.
192     protected String mRootDir = null;
193     protected IConfiguration mConfig;
194     private IInvocationContext mContext;
195 
196     @Override
setInvocationContext(IInvocationContext invocationContext)197     public void setInvocationContext(IInvocationContext invocationContext) {
198         mContext = invocationContext;
199     }
200 
201     @Override
setConfiguration(IConfiguration configuration)202     public void setConfiguration(IConfiguration configuration) {
203         mConfig = configuration;
204     }
205 
setProtoReporting(boolean protoReporting)206     protected void setProtoReporting(boolean protoReporting) {
207         mUseProtoReporting = protoReporting;
208     }
209 
210     /**
211      * Set use-event-streaming.
212      *
213      * Exposed for unit testing.
214      */
setEventStreaming(boolean eventStreaming)215     protected void setEventStreaming(boolean eventStreaming) {
216         mEventStreaming = eventStreaming;
217     }
218 
219     /**
220      * Set IRunUtil.
221      *
222      * Exposed for unit testing.
223      */
setRunUtil(IRunUtil runUtil)224     protected void setRunUtil(IRunUtil runUtil) {
225         mRunUtil = runUtil;
226     }
227 
228     /** Returns the {@link IRunUtil} that will be used for the subprocess command. */
getRunUtil()229     protected IRunUtil getRunUtil() {
230         return mRunUtil;
231     }
232 
233     /**
234      * Setup before running the test.
235      */
preRun()236     protected void preRun() {
237         Assert.assertNotNull(mBuildInfo);
238         Assert.assertNotNull(mConfigName);
239         IFolderBuildInfo tfBuild = (IFolderBuildInfo) mBuildInfo;
240         File rootDirFile = tfBuild.getRootDir();
241         mRootDir = rootDirFile.getAbsolutePath();
242         String jarClasspath = "";
243         List<String> paths = new ArrayList<>();
244         for (String jar : TRADEFED_JARS) {
245             File f = FileUtil.findFile(rootDirFile, jar);
246             if (f != null && f.exists()) {
247                 paths.add(f.getAbsolutePath());
248             }
249         }
250         jarClasspath = String.join(":", paths);
251 
252         mCmdArgs = new ArrayList<String>();
253         mCmdArgs.add(getJava());
254 
255         try {
256             mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId());
257             mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath()));
258         } catch (IOException e) {
259             CLog.e(e);
260             throw new RuntimeException(e);
261         }
262 
263         addJavaArguments(mCmdArgs);
264 
265         if (mRemoteDebug) {
266             mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088");
267         }
268         // This prevent the illegal reflective access warnings by allowing some packages.
269         if (!mDisableJavaOpens) {
270             for (String modulePackage : mAddOpens) {
271                 mCmdArgs.add("--add-opens=" + modulePackage + "=ALL-UNNAMED");
272             }
273         }
274         mCmdArgs.add("-cp");
275 
276         mCmdArgs.add(jarClasspath);
277         mCmdArgs.add("com.android.tradefed.command.CommandRunner");
278         mCmdArgs.add(mConfigName);
279 
280         Integer shardCount = mConfig.getCommandOptions().getShardCount();
281         if (mLocalShardingMode && shardCount != null & shardCount > 1) {
282             mCmdArgs.add("--shard-count");
283             mCmdArgs.add(Integer.toString(shardCount));
284         }
285 
286         if (!mSubParams.isEmpty()) {
287             mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mSubParams));
288         }
289 
290         // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
291         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
292         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
293         mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR);
294         mRunUtil.unsetEnvVariable(DelegatedInvocationExecution.DELEGATED_MODE_VAR);
295         for (String variable : AutomatedReporters.REPORTER_MAPPING) {
296             mRunUtil.unsetEnvVariable(variable);
297         }
298         // Handle feature server
299         getRunUtil().unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER);
300         getRunUtil().unsetEnvVariable(TradefedFeatureServer.TF_SERVICE_PORT);
301         getRunUtil().setEnvVariablePriority(EnvPriority.SET);
302         getRunUtil()
303                 .setEnvVariable(
304                         TradefedFeatureServer.TF_SERVICE_PORT,
305                         Integer.toString(TradefedFeatureServer.getPort()));
306 
307         if (mGlobalConfig == null) {
308             // If the global configuration is not set in option, create a filtered global
309             // configuration for subprocess to use.
310             try {
311                 File filteredGlobalConfig =
312                         GlobalConfiguration.getInstance().cloneConfigWithFilter();
313                 mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath();
314                 mGlobalConfig = mFilteredGlobalConfig;
315             } catch (IOException e) {
316                 CLog.e("Failed to create filtered global configuration");
317                 CLog.e(e);
318             }
319         }
320         if (mGlobalConfig != null) {
321             // We allow overriding this global config and then set it for the subprocess.
322             mRunUtil.setEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig);
323         }
324     }
325 
326     /**
327      * Allow to add extra java parameters to the subprocess invocation.
328      *
329      * @param args the current list of arguments to which we need to add the extra ones.
330      */
addJavaArguments(List<String> args)331     protected void addJavaArguments(List<String> args) {}
332 
333     /**
334      * Actions to take after the TF test is finished.
335      *
336      * @param listener the original {@link ITestInvocationListener} where to report results.
337      * @param exception True if exception was raised inside the test.
338      * @param elapsedTime the time taken to run the tests.
339      */
postRun(ITestInvocationListener listener, boolean exception, long elapsedTime)340     protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {}
341 
342     /** Pipe to the subprocess the invocation-data so that it can use them if needed. */
addInvocationData()343     private void addInvocationData() {
344         if (!mInjectInvocationData) {
345             return;
346         }
347         UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData();
348         for (String key : data.keySet()) {
349             for (String value : data.get(key)) {
350                 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
351                 mCmdArgs.add(key);
352                 mCmdArgs.add(value);
353             }
354         }
355         // Finally add one last more to tag the subprocess
356         mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
357         mCmdArgs.add(SUBPROCESS_TAG_NAME);
358         mCmdArgs.add("true");
359         // Tag the parent invocation
360         mBuildInfo.addBuildAttribute(PARENT_PROC_TAG_NAME, "true");
361     }
362 
363     /** {@inheritDoc} */
364     @Override
run(TestInformation testInfo, ITestInvocationListener listener)365     public void run(TestInformation testInfo, ITestInvocationListener listener)
366             throws DeviceNotAvailableException {
367         preRun();
368         addInvocationData();
369 
370         File stdoutFile = null;
371         File stderrFile = null;
372         File eventFile = null;
373         SubprocessTestResultsParser eventParser = null;
374         StreamProtoReceiver protoReceiver = null;
375         FileOutputStream stdout = null;
376         FileOutputStream stderr = null;
377 
378         boolean exception = false;
379         long startTime = 0L;
380         long elapsedTime = -1L;
381         try {
382             stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log");
383             stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log");
384             stderr = new FileOutputStream(stderrFile);
385             stdout = new FileOutputStream(stdoutFile);
386             if (mUseProtoReporting) {
387                 // Skip merging properties to avoid contaminating metrics with unit tests
388                 protoReceiver =
389                         new StreamProtoReceiver(
390                                 listener, mContext, false, false, true, "subprocess-", false);
391                 mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION);
392                 mCmdArgs.add(Integer.toString(protoReceiver.getSocketServerPort()));
393             } else {
394                 eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext);
395                 if (mEventStreaming) {
396                     mCmdArgs.add("--subprocess-report-port");
397                     mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort()));
398                 } else {
399                     eventFile = FileUtil.createTempFile("event_subprocess_", ".log");
400                     mCmdArgs.add("--subprocess-report-file");
401                     mCmdArgs.add(eventFile.getAbsolutePath());
402                 }
403                 eventParser.setIgnoreTestLog(mIgnoreTestLog);
404             }
405             startTime = System.currentTimeMillis();
406             CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout,
407                     stderr, mCmdArgs.toArray(new String[0]));
408 
409             if (eventParser != null) {
410                 if (eventParser.getStartTime() != null) {
411                     startTime = eventParser.getStartTime();
412                 }
413                 elapsedTime = System.currentTimeMillis() - startTime;
414                 // We possibly allow for a little more time if the thread is still processing
415                 // events.
416                 if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
417                     elapsedTime = -1L;
418                     throw new RuntimeException(
419                             String.format(
420                                     "Event receiver thread did not complete:" + "\n%s",
421                                     FileUtil.readStringFromFile(stderrFile)));
422                 }
423             } else if (protoReceiver != null) {
424                 if (!protoReceiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
425                     elapsedTime = -1L;
426                     throw new RuntimeException(
427                             String.format(
428                                     "Event receiver thread did not complete:" + "\n%s",
429                                     FileUtil.readStringFromFile(stderrFile)));
430                 }
431                 protoReceiver.completeModuleEvents();
432             }
433             if (result.getStatus().equals(CommandStatus.SUCCESS)) {
434                 CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
435                 testCleanStdErr(stderrFile, listener);
436             } else {
437                 CLog.w("Failed ran TF tests for build %s, status %s",
438                         mBuildInfo.getBuildId(), result.getStatus());
439                 CLog.v(
440                         "TF tests output:\nstdout:\n%s\nstderr:\n%s",
441                         result.getStdout(), result.getStderr());
442                 exception = true;
443                 String errMessage = null;
444                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
445                     errMessage = String.format("Timeout after %s",
446                             TimeUtil.formatElapsedTime(mMaxTfRunTime));
447                     throw new HarnessRuntimeException(
448                             String.format(
449                                     "%s Tests subprocess failed due to:\n%s\n",
450                                     mConfigName, errMessage),
451                             InfraErrorIdentifier.INVOCATION_TIMEOUT);
452                 } else if (eventParser != null && !eventParser.reportedInvocationFailed()) {
453                     SubprocessExceptionParser.handleStderrException(result);
454                 }
455             }
456         } catch (IOException e) {
457             exception = true;
458             throw new RuntimeException(e);
459         } finally {
460             StreamUtil.close(stdout);
461             StreamUtil.close(stderr);
462             logAndCleanFile(stdoutFile, listener);
463             logAndCleanFile(stderrFile, listener);
464             if (eventFile != null) {
465                 eventParser.parseFile(eventFile);
466                 logAndCleanFile(eventFile, listener);
467             }
468             StreamUtil.close(eventParser);
469             StreamUtil.close(protoReceiver);
470 
471             if (mGlobalConfig != null && new File(mGlobalConfig).exists()) {
472                 logAndCleanFile(new File(mGlobalConfig), listener);
473             }
474 
475             postRun(listener, exception, elapsedTime);
476 
477             if (mTmpDir != null) {
478                 FileUtil.recursiveDelete(mTmpDir);
479             }
480 
481             if (mFilteredGlobalConfig != null) {
482                 FileUtil.deleteFile(new File(mFilteredGlobalConfig));
483             }
484         }
485     }
486 
487     /**
488      * Log the content of given file to listener, then remove the file.
489      *
490      * @param fileToExport the {@link File} pointing to the file to log.
491      * @param listener the {@link ITestInvocationListener} where to report the test.
492      */
logAndCleanFile(File fileToExport, ITestInvocationListener listener)493     private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) {
494         if (fileToExport == null) {
495             return;
496         }
497 
498         try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport, true)) {
499             listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream);
500         } catch (RuntimeException e) {
501             CLog.e(e);
502         }
503     }
504 
505     /**
506      * {@inheritDoc}
507      */
508     @Override
setBuild(IBuildInfo buildInfo)509     public void setBuild(IBuildInfo buildInfo) {
510         mBuildInfo = buildInfo;
511     }
512 
513     /**
514      * Extra test to ensure no abnormal logging is made to stderr when all the tests pass.
515      *
516      * @param stdErrFile the stderr log file of the subprocess.
517      * @param listener the {@link ITestInvocationListener} where to report the test.
518      */
testCleanStdErr(File stdErrFile, ITestInvocationListener listener)519     private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener)
520             throws IOException {
521         if (mDisableStderrTest) {
522             return;
523         }
524         listener.testRunStarted("StdErr", 1);
525         TestDescription tid = new TestDescription("stderr-test", "checkIsEmpty");
526         listener.testStarted(tid);
527         if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) {
528             String trace =
529                     String.format(
530                             "Found some output in stderr:\n%s",
531                             FileUtil.readStringFromFile(stdErrFile));
532             listener.testFailed(tid, FailureDescription.create(trace));
533         }
534         listener.testEnded(tid, new HashMap<String, Metric>());
535         listener.testRunEnded(0, new HashMap<String, Metric>());
536     }
537 
getJava()538     protected String getJava() {
539         return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath();
540     }
541 }
542