• 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.invoker.IInvocationContext;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
28 import com.android.tradefed.result.FileInputStreamSource;
29 import com.android.tradefed.result.ITestInvocationListener;
30 import com.android.tradefed.result.LogDataType;
31 import com.android.tradefed.result.TestDescription;
32 import com.android.tradefed.util.CommandResult;
33 import com.android.tradefed.util.CommandStatus;
34 import com.android.tradefed.util.FileUtil;
35 import com.android.tradefed.util.IRunUtil;
36 import com.android.tradefed.util.IRunUtil.EnvPriority;
37 import com.android.tradefed.util.RunUtil;
38 import com.android.tradefed.util.StreamUtil;
39 import com.android.tradefed.util.SubprocessTestResultsParser;
40 import com.android.tradefed.util.TimeUtil;
41 import com.android.tradefed.util.UniqueMultiMap;
42 
43 import org.junit.Assert;
44 
45 import java.io.File;
46 import java.io.FileOutputStream;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 
52 /**
53  * A {@link IRemoteTest} for running tests against a separate TF installation.
54  *
55  * <p>Launches an external java process to run the tests. Used for running the TF unit or functional
56  * tests continuously.
57  */
58 public abstract class SubprocessTfLauncher
59         implements IBuildReceiver, IInvocationContextReceiver, IRemoteTest, IConfigurationReceiver {
60 
61     /** The tag that will be passed to the TF subprocess to differentiate it */
62     public static final String SUBPROCESS_TAG_NAME = "subprocess";
63 
64     public static final String PARENT_PROC_TAG_NAME = "parentprocess";
65     /** Env. variable that affects adb selection. */
66     public static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL";
67 
68     @Option(name = "max-run-time", description =
69             "The maximum time to allow for a TF test run.", isTimeVal = true)
70     private long mMaxTfRunTime = 20 * 60 * 1000;
71 
72     @Option(name = "remote-debug", description =
73             "Start the TF java process in remote debug mode.")
74     private boolean mRemoteDebug = false;
75 
76     @Option(name = "config-name", description = "The config that runs the TF tests")
77     private String mConfigName;
78 
79     @Option(name = "use-event-streaming", description = "Use a socket to receive results as they"
80             + "arrived instead of using a temporary file and parsing at the end.")
81     private boolean mEventStreaming = true;
82 
83     @Option(name = "sub-global-config", description = "The global config name to pass to the"
84             + "sub process, can be local or from jar resources. Be careful of conflicts with "
85             + "parent process.")
86     private String mGlobalConfig = null;
87 
88     @Option(
89             name = "inject-invocation-data",
90             description = "Pass the invocation-data to the subprocess if enabled.")
91     private boolean mInjectInvocationData = true;
92 
93     // Temp global configuration filtered from the parent process.
94     private String mFilteredGlobalConfig = null;
95 
96     /** Timeout to wait for the events received from subprocess to finish being processed.*/
97     private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000;
98 
99     protected IRunUtil mRunUtil =  new RunUtil();
100 
101     protected IBuildInfo mBuildInfo = null;
102     // Temp directory to run the TF process.
103     protected File mTmpDir = null;
104     // List of command line arguments to run the TF process.
105     protected List<String> mCmdArgs = null;
106     // The absolute path to the build's root directory.
107     protected String mRootDir = null;
108     protected IConfiguration mConfig;
109     private IInvocationContext mContext;
110 
111     @Override
setInvocationContext(IInvocationContext invocationContext)112     public void setInvocationContext(IInvocationContext invocationContext) {
113         mContext = invocationContext;
114     }
115 
116     @Override
setConfiguration(IConfiguration configuration)117     public void setConfiguration(IConfiguration configuration) {
118         mConfig = configuration;
119     }
120 
121     /**
122      * Set use-event-streaming.
123      *
124      * Exposed for unit testing.
125      */
setEventStreaming(boolean eventStreaming)126     protected void setEventStreaming(boolean eventStreaming) {
127         mEventStreaming = eventStreaming;
128     }
129 
130     /**
131      * Set IRunUtil.
132      *
133      * Exposed for unit testing.
134      */
setRunUtil(IRunUtil runUtil)135     protected void setRunUtil(IRunUtil runUtil) {
136         mRunUtil = runUtil;
137     }
138 
139     /** Returns the {@link IRunUtil} that will be used for the subprocess command. */
getRunUtil()140     protected IRunUtil getRunUtil() {
141         return mRunUtil;
142     }
143 
144     /**
145      * Setup before running the test.
146      */
preRun()147     protected void preRun() {
148         Assert.assertNotNull(mBuildInfo);
149         Assert.assertNotNull(mConfigName);
150         IFolderBuildInfo tfBuild = (IFolderBuildInfo) mBuildInfo;
151         mRootDir = tfBuild.getRootDir().getAbsolutePath();
152         String jarClasspath = FileUtil.getPath(mRootDir, "*");
153 
154         mCmdArgs = new ArrayList<String>();
155         mCmdArgs.add("java");
156 
157         try {
158             mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId());
159             mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath()));
160         } catch (IOException e) {
161             CLog.e(e);
162             throw new RuntimeException(e);
163         }
164 
165         addJavaArguments(mCmdArgs);
166 
167         if (mRemoteDebug) {
168             mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088");
169         }
170         // FIXME: b/72742216: This prevent the illegal reflective access
171         mCmdArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED");
172         mCmdArgs.add("-cp");
173 
174         mCmdArgs.add(jarClasspath);
175         mCmdArgs.add("com.android.tradefed.command.CommandRunner");
176         mCmdArgs.add(mConfigName);
177 
178         // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
179         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
180         mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
181         mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR);
182 
183         if (mGlobalConfig == null) {
184             // If the global configuration is not set in option, create a filtered global
185             // configuration for subprocess to use.
186             try {
187                 File filteredGlobalConfig =
188                         GlobalConfiguration.getInstance().cloneConfigWithFilter();
189                 mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath();
190                 mGlobalConfig = mFilteredGlobalConfig;
191             } catch (IOException e) {
192                 CLog.e("Failed to create filtered global configuration");
193                 CLog.e(e);
194             }
195         }
196         if (mGlobalConfig != null) {
197             // We allow overriding this global config and then set it for the subprocess.
198             mRunUtil.setEnvVariablePriority(EnvPriority.SET);
199             mRunUtil.setEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig);
200         }
201     }
202 
203     /**
204      * Allow to add extra java parameters to the subprocess invocation.
205      *
206      * @param args the current list of arguments to which we need to add the extra ones.
207      */
addJavaArguments(List<String> args)208     protected void addJavaArguments(List<String> args) {}
209 
210     /**
211      * Actions to take after the TF test is finished.
212      *
213      * @param listener the original {@link ITestInvocationListener} where to report results.
214      * @param exception True if exception was raised inside the test.
215      * @param elapsedTime the time taken to run the tests.
216      */
postRun(ITestInvocationListener listener, boolean exception, long elapsedTime)217     protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {}
218 
219     /** Pipe to the subprocess the invocation-data so that it can use them if needed. */
addInvocationData()220     private void addInvocationData() {
221         if (!mInjectInvocationData) {
222             return;
223         }
224         UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData();
225         for (String key : data.keySet()) {
226             for (String value : data.get(key)) {
227                 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
228                 mCmdArgs.add(key);
229                 mCmdArgs.add(value);
230             }
231         }
232         // Finally add one last more to tag the subprocess
233         mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA);
234         mCmdArgs.add(SUBPROCESS_TAG_NAME);
235         mCmdArgs.add("true");
236         // Tag the parent invocation
237         mBuildInfo.addBuildAttribute(PARENT_PROC_TAG_NAME, "true");
238     }
239 
240     /** {@inheritDoc} */
241     @Override
run(ITestInvocationListener listener)242     public void run(ITestInvocationListener listener) {
243         preRun();
244         addInvocationData();
245 
246         File stdoutFile = null;
247         File stderrFile = null;
248         File eventFile = null;
249         SubprocessTestResultsParser eventParser = null;
250         FileOutputStream stdout = null;
251         FileOutputStream stderr = null;
252 
253         boolean exception = false;
254         long startTime = 0l;
255         long elapsedTime = -1l;
256         try {
257             stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log");
258             stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log");
259             stderr = new FileOutputStream(stderrFile);
260             stdout = new FileOutputStream(stdoutFile);
261 
262             eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext);
263             if (mEventStreaming) {
264                 mCmdArgs.add("--subprocess-report-port");
265                 mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort()));
266             } else {
267                 eventFile = FileUtil.createTempFile("event_subprocess_", ".log");
268                 mCmdArgs.add("--subprocess-report-file");
269                 mCmdArgs.add(eventFile.getAbsolutePath());
270             }
271             startTime = System.currentTimeMillis();
272             CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout,
273                     stderr, mCmdArgs.toArray(new String[0]));
274             if (eventParser.getStartTime() != null) {
275                 startTime = eventParser.getStartTime();
276             }
277             elapsedTime = System.currentTimeMillis() - startTime;
278             // We possibly allow for a little more time if the thread is still processing events.
279             if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) {
280                 elapsedTime = -1l;
281                 throw new RuntimeException(String.format("Event receiver thread did not complete:"
282                         + "\n%s", FileUtil.readStringFromFile(stderrFile)));
283             }
284             if (result.getStatus().equals(CommandStatus.SUCCESS)) {
285                 CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
286                 testCleanStdErr(stderrFile, listener);
287             } else {
288                 CLog.w("Failed ran TF tests for build %s, status %s",
289                         mBuildInfo.getBuildId(), result.getStatus());
290                 CLog.v("TF tests output:\nstdout:\n%s\nstderror:\n%s",
291                         result.getStdout(), result.getStderr());
292                 exception = true;
293                 String errMessage = null;
294                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
295                     errMessage = String.format("Timeout after %s",
296                             TimeUtil.formatElapsedTime(mMaxTfRunTime));
297                 } else {
298                     errMessage = FileUtil.readStringFromFile(stderrFile);
299                 }
300                 throw new RuntimeException(
301                         String.format("%s Tests subprocess failed due to:\n%s\n", mConfigName,
302                                 errMessage));
303             }
304         } catch (IOException e) {
305             exception = true;
306             throw new RuntimeException(e);
307         } finally {
308             StreamUtil.close(stdout);
309             StreamUtil.close(stderr);
310             logAndCleanFile(stdoutFile, listener);
311             logAndCleanFile(stderrFile, listener);
312             if (eventFile != null) {
313                 eventParser.parseFile(eventFile);
314                 logAndCleanFile(eventFile, listener);
315             }
316             StreamUtil.close(eventParser);
317 
318             postRun(listener, exception, elapsedTime);
319 
320             if (mTmpDir != null) {
321                 FileUtil.recursiveDelete(mTmpDir);
322             }
323 
324             if (mFilteredGlobalConfig != null) {
325                 FileUtil.deleteFile(new File(mFilteredGlobalConfig));
326             }
327         }
328     }
329 
330     /**
331      * Log the content of given file to listener, then remove the file.
332      *
333      * @param fileToExport the {@link File} pointing to the file to log.
334      * @param listener the {@link ITestInvocationListener} where to report the test.
335      */
logAndCleanFile(File fileToExport, ITestInvocationListener listener)336     private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) {
337         if (fileToExport == null)
338             return;
339 
340         try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport)) {
341             listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream);
342         }
343         FileUtil.deleteFile(fileToExport);
344     }
345 
346     /**
347      * {@inheritDoc}
348      */
349     @Override
setBuild(IBuildInfo buildInfo)350     public void setBuild(IBuildInfo buildInfo) {
351         mBuildInfo = buildInfo;
352     }
353 
354     /**
355      * Extra test to ensure no abnormal logging is made to stderr when all the tests pass.
356      *
357      * @param stdErrFile the stderr log file of the subprocess.
358      * @param listener the {@link ITestInvocationListener} where to report the test.
359      */
testCleanStdErr(File stdErrFile, ITestInvocationListener listener)360     private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener)
361             throws IOException {
362         listener.testRunStarted("StdErr", 1);
363         TestDescription tid = new TestDescription("stderr-test", "checkIsEmpty");
364         listener.testStarted(tid);
365         if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) {
366             String trace =
367                     String.format(
368                             "Found some output in stderr:\n%s",
369                             FileUtil.readStringFromFile(stdErrFile));
370             listener.testFailed(tid, trace);
371         }
372         listener.testEnded(tid, new HashMap<String, Metric>());
373         listener.testRunEnded(0, new HashMap<String, Metric>());
374     }
375 }
376