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                             // libs
161                             "tools-common-prebuilt.jar",
162                             // jar in older branches
163                             "tf-prod-tests.jar",
164                             "tf-prod-metatests.jar",
165                             // Aosp contrib jars
166                             "tradefed-contrib.jar",
167                             "tf-contrib-tests.jar",
168                             // Google Tf jars
169                             "google-tf-prod-tests.jar",
170                             "google-tf-prod-metatests.jar",
171                             "google-tradefed.jar",
172                             "google-tradefed-tests.jar",
173                             // Google contrib jars
174                             "google-tradefed-contrib.jar",
175                             // Older jar required for coverage tests
176                             "jack-jacoco-reporter.jar",
177                             "emmalib.jar"));
178 
179     /** Timeout to wait for the events received from subprocess to finish being processed.*/
180     private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
181 
182     protected IRunUtil mRunUtil =  new RunUtil();
183 
184     protected IBuildInfo mBuildInfo = null;
185     // Temp directory to run the TF process.
186     protected File mTmpDir = null;
187     // List of command line arguments to run the TF process.
188     protected List<String> mCmdArgs = null;
189     // The absolute path to the build's root directory.
190     protected String mRootDir = null;
191     protected IConfiguration mConfig;
192     private IInvocationContext mContext;
193 
194     @Override
setInvocationContext(IInvocationContext invocationContext)195     public void setInvocationContext(IInvocationContext invocationContext) {
196         mContext = invocationContext;
197     }
198 
199     @Override
setConfiguration(IConfiguration configuration)200     public void setConfiguration(IConfiguration configuration) {
201         mConfig = configuration;
202     }
203 
setProtoReporting(boolean protoReporting)204     protected void setProtoReporting(boolean protoReporting) {
205         mUseProtoReporting = protoReporting;
206     }
207 
208     /**
209      * Set use-event-streaming.
210      *
211      * Exposed for unit testing.
212      */
setEventStreaming(boolean eventStreaming)213     protected void setEventStreaming(boolean eventStreaming) {
214         mEventStreaming = eventStreaming;
215     }
216 
217     /**
218      * Set IRunUtil.
219      *
220      * Exposed for unit testing.
221      */
setRunUtil(IRunUtil runUtil)222     protected void setRunUtil(IRunUtil runUtil) {
223         mRunUtil = runUtil;
224     }
225 
226     /** Returns the {@link IRunUtil} that will be used for the subprocess command. */
getRunUtil()227     protected IRunUtil getRunUtil() {
228         return mRunUtil;
229     }
230 
231     /**
232      * Setup before running the test.
233      */
preRun()234     protected void preRun() {
235         Assert.assertNotNull(mBuildInfo);
236         Assert.assertNotNull(mConfigName);
237         IFolderBuildInfo tfBuild = (IFolderBuildInfo) mBuildInfo;
238         File rootDirFile = tfBuild.getRootDir();
239         mRootDir = rootDirFile.getAbsolutePath();
240         String jarClasspath = "";
241         List<String> paths = new ArrayList<>();
242         for (String jar : TRADEFED_JARS) {
243             File f = FileUtil.findFile(rootDirFile, jar);
244             if (f != null && f.exists()) {
245                 paths.add(f.getAbsolutePath());
246             }
247         }
248         jarClasspath = String.join(":", paths);
249 
250         mCmdArgs = new ArrayList<String>();
251         mCmdArgs.add(getJava());
252 
253         try {
254             mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId());
255             mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath()));
256         } catch (IOException e) {
257             CLog.e(e);
258             throw new RuntimeException(e);
259         }
260 
261         addJavaArguments(mCmdArgs);
262 
263         if (mRemoteDebug) {
264             mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088");
265         }
266         // This prevent the illegal reflective access warnings by allowing some packages.
267         if (!mDisableJavaOpens) {
268             for (String modulePackage : mAddOpens) {
269                 mCmdArgs.add("--add-opens=" + modulePackage + "=ALL-UNNAMED");
270             }
271         }
272         mCmdArgs.add("-cp");
273 
274         mCmdArgs.add(jarClasspath);
275         mCmdArgs.add("com.android.tradefed.command.CommandRunner");
276         mCmdArgs.add(mConfigName);
277 
278         Integer shardCount = mConfig.getCommandOptions().getShardCount();
279         if (mLocalShardingMode && shardCount != null & shardCount > 1) {
280             mCmdArgs.add("--shard-count");
281             mCmdArgs.add(Integer.toString(shardCount));
282         }
283 
284         if (!mSubParams.isEmpty()) {
285             mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mSubParams));
286         }
287 
288         // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
289         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
290         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
291         mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR);
292         mRunUtil.unsetEnvVariable(DelegatedInvocationExecution.DELEGATED_MODE_VAR);
293         for (String variable : AutomatedReporters.REPORTER_MAPPING) {
294             mRunUtil.unsetEnvVariable(variable);
295         }
296         // Handle feature server
297         getRunUtil().unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER);
298         getRunUtil().unsetEnvVariable(TradefedFeatureServer.TF_SERVICE_PORT);
299         getRunUtil().setEnvVariablePriority(EnvPriority.SET);
300         getRunUtil()
301                 .setEnvVariable(
302                         TradefedFeatureServer.TF_SERVICE_PORT,
303                         Integer.toString(TradefedFeatureServer.getPort()));
304 
305         if (mGlobalConfig == null) {
306             // If the global configuration is not set in option, create a filtered global
307             // configuration for subprocess to use.
308             try {
309                 File filteredGlobalConfig =
310                         GlobalConfiguration.getInstance().cloneConfigWithFilter();
311                 mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath();
312                 mGlobalConfig = mFilteredGlobalConfig;
313             } catch (IOException e) {
314                 CLog.e("Failed to create filtered global configuration");
315                 CLog.e(e);
316             }
317         }
318         if (mGlobalConfig != null) {
319             // We allow overriding this global config and then set it for the subprocess.
320             mRunUtil.setEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig);
321         }
322     }
323 
324     /**
325      * Allow to add extra java parameters to the subprocess invocation.
326      *
327      * @param args the current list of arguments to which we need to add the extra ones.
328      */
addJavaArguments(List<String> args)329     protected void addJavaArguments(List<String> args) {}
330 
331     /**
332      * Actions to take after the TF test is finished.
333      *
334      * @param listener the original {@link ITestInvocationListener} where to report results.
335      * @param exception True if exception was raised inside the test.
336      * @param elapsedTime the time taken to run the tests.
337      */
postRun(ITestInvocationListener listener, boolean exception, long elapsedTime)338     protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {}
339 
340     /** Pipe to the subprocess the invocation-data so that it can use them if needed. */
addInvocationData()341     private void addInvocationData() {
342         if (!mInjectInvocationData) {
343             return;
344         }
345         UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData();
346         for (String key : data.keySet()) {
347             for (String value : data.get(key)) {
348                 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
349                 mCmdArgs.add(key);
350                 mCmdArgs.add(value);
351             }
352         }
353         // Finally add one last more to tag the subprocess
354         mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
355         mCmdArgs.add(SUBPROCESS_TAG_NAME);
356         mCmdArgs.add("true");
357         // Tag the parent invocation
358         mBuildInfo.addBuildAttribute(PARENT_PROC_TAG_NAME, "true");
359     }
360 
361     /** {@inheritDoc} */
362     @Override
run(TestInformation testInfo, ITestInvocationListener listener)363     public void run(TestInformation testInfo, ITestInvocationListener listener)
364             throws DeviceNotAvailableException {
365         preRun();
366         addInvocationData();
367 
368         File stdoutFile = null;
369         File stderrFile = null;
370         File eventFile = null;
371         SubprocessTestResultsParser eventParser = null;
372         StreamProtoReceiver protoReceiver = null;
373         FileOutputStream stdout = null;
374         FileOutputStream stderr = null;
375 
376         boolean exception = false;
377         long startTime = 0L;
378         long elapsedTime = -1L;
379         try {
380             stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log");
381             stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log");
382             stderr = new FileOutputStream(stderrFile);
383             stdout = new FileOutputStream(stdoutFile);
384             if (mUseProtoReporting) {
385                 // Skip merging properties to avoid contaminating metrics with unit tests
386                 protoReceiver =
387                         new StreamProtoReceiver(
388                                 listener, mContext, false, false, true, "subprocess-", false);
389                 mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION);
390                 mCmdArgs.add(Integer.toString(protoReceiver.getSocketServerPort()));
391             } else {
392                 eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext);
393                 if (mEventStreaming) {
394                     mCmdArgs.add("--subprocess-report-port");
395                     mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort()));
396                 } else {
397                     eventFile = FileUtil.createTempFile("event_subprocess_", ".log");
398                     mCmdArgs.add("--subprocess-report-file");
399                     mCmdArgs.add(eventFile.getAbsolutePath());
400                 }
401                 eventParser.setIgnoreTestLog(mIgnoreTestLog);
402             }
403             startTime = System.currentTimeMillis();
404             CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout,
405                     stderr, mCmdArgs.toArray(new String[0]));
406 
407             if (eventParser != null) {
408                 if (eventParser.getStartTime() != null) {
409                     startTime = eventParser.getStartTime();
410                 }
411                 elapsedTime = System.currentTimeMillis() - startTime;
412                 // We possibly allow for a little more time if the thread is still processing
413                 // events.
414                 if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
415                     elapsedTime = -1L;
416                     throw new RuntimeException(
417                             String.format(
418                                     "Event receiver thread did not complete:" + "\n%s",
419                                     FileUtil.readStringFromFile(stderrFile)));
420                 }
421             } else if (protoReceiver != null) {
422                 if (!protoReceiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
423                     elapsedTime = -1L;
424                     throw new RuntimeException(
425                             String.format(
426                                     "Event receiver thread did not complete:" + "\n%s",
427                                     FileUtil.readStringFromFile(stderrFile)));
428                 }
429                 protoReceiver.completeModuleEvents();
430             }
431             if (result.getStatus().equals(CommandStatus.SUCCESS)) {
432                 CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
433                 testCleanStdErr(stderrFile, listener);
434             } else {
435                 CLog.w("Failed ran TF tests for build %s, status %s",
436                         mBuildInfo.getBuildId(), result.getStatus());
437                 CLog.v(
438                         "TF tests output:\nstdout:\n%s\nstderr:\n%s",
439                         result.getStdout(), result.getStderr());
440                 exception = true;
441                 String errMessage = null;
442                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
443                     errMessage = String.format("Timeout after %s",
444                             TimeUtil.formatElapsedTime(mMaxTfRunTime));
445                     throw new HarnessRuntimeException(
446                             String.format(
447                                     "%s Tests subprocess failed due to:\n%s\n",
448                                     mConfigName, errMessage),
449                             InfraErrorIdentifier.INVOCATION_TIMEOUT);
450                 } else if (eventParser != null && !eventParser.reportedInvocationFailed()) {
451                     SubprocessExceptionParser.handleStderrException(result);
452                 }
453             }
454         } catch (IOException e) {
455             exception = true;
456             throw new RuntimeException(e);
457         } finally {
458             StreamUtil.close(stdout);
459             StreamUtil.close(stderr);
460             logAndCleanFile(stdoutFile, listener);
461             logAndCleanFile(stderrFile, listener);
462             if (eventFile != null) {
463                 eventParser.parseFile(eventFile);
464                 logAndCleanFile(eventFile, listener);
465             }
466             StreamUtil.close(eventParser);
467             StreamUtil.close(protoReceiver);
468 
469             if (mGlobalConfig != null) {
470                 logAndCleanFile(new File(mGlobalConfig), listener);
471             }
472 
473             postRun(listener, exception, elapsedTime);
474 
475             if (mTmpDir != null) {
476                 FileUtil.recursiveDelete(mTmpDir);
477             }
478 
479             if (mFilteredGlobalConfig != null) {
480                 FileUtil.deleteFile(new File(mFilteredGlobalConfig));
481             }
482         }
483     }
484 
485     /**
486      * Log the content of given file to listener, then remove the file.
487      *
488      * @param fileToExport the {@link File} pointing to the file to log.
489      * @param listener the {@link ITestInvocationListener} where to report the test.
490      */
logAndCleanFile(File fileToExport, ITestInvocationListener listener)491     private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) {
492         if (fileToExport == null) {
493             return;
494         }
495 
496         try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport)) {
497             listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream);
498         }
499         FileUtil.deleteFile(fileToExport);
500     }
501 
502     /**
503      * {@inheritDoc}
504      */
505     @Override
setBuild(IBuildInfo buildInfo)506     public void setBuild(IBuildInfo buildInfo) {
507         mBuildInfo = buildInfo;
508     }
509 
510     /**
511      * Extra test to ensure no abnormal logging is made to stderr when all the tests pass.
512      *
513      * @param stdErrFile the stderr log file of the subprocess.
514      * @param listener the {@link ITestInvocationListener} where to report the test.
515      */
testCleanStdErr(File stdErrFile, ITestInvocationListener listener)516     private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener)
517             throws IOException {
518         if (mDisableStderrTest) {
519             return;
520         }
521         listener.testRunStarted("StdErr", 1);
522         TestDescription tid = new TestDescription("stderr-test", "checkIsEmpty");
523         listener.testStarted(tid);
524         if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) {
525             String trace =
526                     String.format(
527                             "Found some output in stderr:\n%s",
528                             FileUtil.readStringFromFile(stdErrFile));
529             listener.testFailed(tid, FailureDescription.create(trace));
530         }
531         listener.testEnded(tid, new HashMap<String, Metric>());
532         listener.testRunEnded(0, new HashMap<String, Metric>());
533     }
534 
getJava()535     protected String getJava() {
536         return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath();
537     }
538 }
539