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.result;
17 
18 import com.android.tradefed.config.Option;
19 import com.android.tradefed.invoker.IInvocationContext;
20 import com.android.tradefed.log.LogUtil.CLog;
21 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
22 import com.android.tradefed.result.retry.ISupportGranularResults;
23 import com.android.tradefed.result.skipped.SkipReason;
24 import com.android.tradefed.util.FileUtil;
25 import com.android.tradefed.util.StreamUtil;
26 import com.android.tradefed.util.SubprocessEventHelper.BaseTestEventInfo;
27 import com.android.tradefed.util.SubprocessEventHelper.FailedTestEventInfo;
28 import com.android.tradefed.util.SubprocessEventHelper.InvocationEndedEventInfo;
29 import com.android.tradefed.util.SubprocessEventHelper.InvocationFailedEventInfo;
30 import com.android.tradefed.util.SubprocessEventHelper.InvocationStartedEventInfo;
31 import com.android.tradefed.util.SubprocessEventHelper.LogAssociationEventInfo;
32 import com.android.tradefed.util.SubprocessEventHelper.SkippedTestEventInfo;
33 import com.android.tradefed.util.SubprocessEventHelper.TestEndedEventInfo;
34 import com.android.tradefed.util.SubprocessEventHelper.TestLogEventInfo;
35 import com.android.tradefed.util.SubprocessEventHelper.TestModuleStartedEventInfo;
36 import com.android.tradefed.util.SubprocessEventHelper.TestRunEndedEventInfo;
37 import com.android.tradefed.util.SubprocessEventHelper.TestRunFailedEventInfo;
38 import com.android.tradefed.util.SubprocessEventHelper.TestRunStartedEventInfo;
39 import com.android.tradefed.util.SubprocessEventHelper.TestStartedEventInfo;
40 import com.android.tradefed.util.SubprocessTestResultsParser;
41 import com.android.tradefed.util.proto.TfMetricProtoUtil;
42 
43 import org.json.JSONObject;
44 
45 import java.io.File;
46 import java.io.FileWriter;
47 import java.io.IOException;
48 import java.io.PrintWriter;
49 import java.net.Socket;
50 import java.util.HashMap;
51 import java.util.Map;
52 
53 /**
54  * Implements {@link ITestInvocationListener} to be specified as a result_reporter and forward from
55  * the subprocess the results of tests, test runs, test invocations.
56  */
57 public class SubprocessResultsReporter
58         implements ITestInvocationListener,
59                 ILogSaverListener,
60                 AutoCloseable,
61                 ISupportGranularResults {
62 
63     @Option(
64             name = "enable-granular-attempts",
65             description = "Whether to allow SubprocessResultsReporter receiving granular attempts.")
66     private boolean mGranularAttempts = true;
67 
68     @Option(name = "subprocess-report-file", description = "the file where to log the events.")
69     private File mReportFile = null;
70 
71     @Option(name = "subprocess-report-port", description = "the port where to connect to send the"
72             + "events.")
73     private Integer mReportPort = null;
74 
75     @Option(name = "output-test-log", description = "Option to report test logs to parent process.")
76     private boolean mOutputTestlog = false;
77 
78     private IInvocationContext mContext = null;
79     private Socket mReportSocket = null;
80     private Object mLock = new Object();
81     private PrintWriter mPrintWriter = null;
82 
83     private boolean mPrintWarning = true;
84     private boolean mCancelled = false;
85 
86     @Override
supportGranularResults()87     public boolean supportGranularResults() {
88         return mGranularAttempts;
89     }
90 
91     /** {@inheritDoc} */
92     @Override
testAssumptionFailure(TestDescription testId, String trace)93     public void testAssumptionFailure(TestDescription testId, String trace) {
94         FailedTestEventInfo info =
95                 new FailedTestEventInfo(testId.getClassName(), testId.getTestName(), trace);
96         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_ASSUMPTION_FAILURE, info);
97     }
98 
99     /** {@inheritDoc} */
100     @Override
testAssumptionFailure(TestDescription testId, FailureDescription failure)101     public void testAssumptionFailure(TestDescription testId, FailureDescription failure) {
102         FailedTestEventInfo info =
103                 new FailedTestEventInfo(testId.getClassName(), testId.getTestName(), failure);
104         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_ASSUMPTION_FAILURE, info);
105     }
106 
107     /** {@inheritDoc} */
108     @Override
testSkipped(TestDescription testId, SkipReason reason)109     public void testSkipped(TestDescription testId, SkipReason reason) {
110         SkippedTestEventInfo info =
111                 new SkippedTestEventInfo(testId.getClassName(), testId.getTestName(), reason);
112         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_SKIPPED, info);
113     }
114 
115     /** {@inheritDoc} */
116     @Override
testEnded(TestDescription testId, HashMap<String, Metric> metrics)117     public void testEnded(TestDescription testId, HashMap<String, Metric> metrics) {
118         testEnded(testId, System.currentTimeMillis(), metrics);
119     }
120 
121     /** {@inheritDoc} */
122     @Override
testEnded(TestDescription testId, long endTime, HashMap<String, Metric> metrics)123     public void testEnded(TestDescription testId, long endTime, HashMap<String, Metric> metrics) {
124         // TODO: transfer the proto metrics instead of string metrics
125         TestEndedEventInfo info =
126                 new TestEndedEventInfo(
127                         testId.getClassName(),
128                         testId.getTestName(),
129                         endTime,
130                         TfMetricProtoUtil.compatibleConvert(metrics));
131         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_ENDED, info);
132     }
133 
134     /** {@inheritDoc} */
135     @Override
testFailed(TestDescription testId, String reason)136     public void testFailed(TestDescription testId, String reason) {
137         FailedTestEventInfo info =
138                 new FailedTestEventInfo(testId.getClassName(), testId.getTestName(), reason);
139         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_FAILED, info);
140     }
141 
142     /** {@inheritDoc} */
143     @Override
testFailed(TestDescription testId, FailureDescription failure)144     public void testFailed(TestDescription testId, FailureDescription failure) {
145         FailedTestEventInfo info =
146                 new FailedTestEventInfo(testId.getClassName(), testId.getTestName(), failure);
147         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_FAILED, info);
148     }
149 
150     /** {@inheritDoc} */
151     @Override
testIgnored(TestDescription testId)152     public void testIgnored(TestDescription testId) {
153         BaseTestEventInfo info = new BaseTestEventInfo(testId.getClassName(), testId.getTestName());
154         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_IGNORED, info);
155     }
156 
157     /** {@inheritDoc} */
158     @Override
testRunEnded(long time, HashMap<String, Metric> runMetrics)159     public void testRunEnded(long time, HashMap<String, Metric> runMetrics) {
160         // TODO: Transfer the full proto instead of just Strings.
161         TestRunEndedEventInfo info =
162                 new TestRunEndedEventInfo(time, TfMetricProtoUtil.compatibleConvert(runMetrics));
163         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_RUN_ENDED, info);
164     }
165 
166     /** {@inheritDoc} */
167     @Override
testRunFailed(String reason)168     public void testRunFailed(String reason) {
169         TestRunFailedEventInfo info = new TestRunFailedEventInfo(reason);
170         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_RUN_FAILED, info);
171     }
172 
173     /** {@inheritDoc} */
174     @Override
testRunFailed(FailureDescription failure)175     public void testRunFailed(FailureDescription failure) {
176         TestRunFailedEventInfo info = new TestRunFailedEventInfo(failure);
177         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_RUN_FAILED, info);
178     }
179 
180     /** {@inheritDoc} */
181     @Override
testRunStarted(String runName, int testCount)182     public void testRunStarted(String runName, int testCount) {
183         testRunStarted(runName, testCount, 0);
184     }
185 
186     /** {@inheritDoc} */
187     @Override
testRunStarted(String runName, int testCount, int attemptNumber)188     public void testRunStarted(String runName, int testCount, int attemptNumber) {
189         testRunStarted(runName, testCount, attemptNumber, System.currentTimeMillis());
190     }
191 
192     /** {@inheritDoc} */
193     @Override
testRunStarted(String runName, int testCount, int attemptNumber, long startTime)194     public void testRunStarted(String runName, int testCount, int attemptNumber, long startTime) {
195         TestRunStartedEventInfo info =
196                 new TestRunStartedEventInfo(runName, testCount, attemptNumber, startTime);
197         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_RUN_STARTED, info);
198     }
199 
200     /**
201      * {@inheritDoc}
202      */
203     @Override
testRunStopped(long arg0)204     public void testRunStopped(long arg0) {
205         // ignore
206     }
207 
208     /** {@inheritDoc} */
209     @Override
testStarted(TestDescription testId)210     public void testStarted(TestDescription testId) {
211         testStarted(testId, System.currentTimeMillis());
212     }
213 
214     /** {@inheritDoc} */
215     @Override
testStarted(TestDescription testId, long startTime)216     public void testStarted(TestDescription testId, long startTime) {
217         TestStartedEventInfo info =
218                 new TestStartedEventInfo(testId.getClassName(), testId.getTestName(), startTime);
219         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_STARTED, info);
220     }
221 
222     /**
223      * {@inheritDoc}
224      */
225     @Override
invocationStarted(IInvocationContext context)226     public void invocationStarted(IInvocationContext context) {
227         InvocationStartedEventInfo info =
228                 new InvocationStartedEventInfo(context.getTestTag(), System.currentTimeMillis());
229         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_STARTED, info);
230         // Save off the context so that we can parse it later during invocation ended.
231         mContext = context;
232     }
233 
234     /** {@inheritDoc} */
235     @Override
testLog(String dataName, LogDataType dataType, InputStreamSource dataStream)236     public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
237         if (!mOutputTestlog || (mReportPort == null && mReportFile == null)) {
238             return;
239         }
240         if (dataStream != null && dataStream.size() != 0) {
241             File tmpFile = null;
242             try {
243                 // put 'subprocess' in front to identify the files.
244                 tmpFile =
245                         FileUtil.createTempFile(
246                                 "subprocess-" + dataName, "." + dataType.getFileExt());
247                 FileUtil.writeToFile(dataStream.createInputStream(), tmpFile);
248                 TestLogEventInfo info = new TestLogEventInfo(dataName, dataType, tmpFile);
249                 printEvent(SubprocessTestResultsParser.StatusKeys.TEST_LOG, info);
250             } catch (IOException e) {
251                 CLog.e(e);
252                 FileUtil.deleteFile(tmpFile);
253             }
254         }
255     }
256 
257     /** {@inheritDoc} */
258     @Override
logAssociation(String dataName, LogFile logFile)259     public void logAssociation(String dataName, LogFile logFile) {
260         LogAssociationEventInfo info = new LogAssociationEventInfo(dataName, logFile);
261         printEvent(SubprocessTestResultsParser.StatusKeys.LOG_ASSOCIATION, info);
262     }
263 
264     /**
265      * {@inheritDoc}
266      */
267     @Override
invocationEnded(long elapsedTime)268     public void invocationEnded(long elapsedTime) {
269         if (mContext == null) {
270             return;
271         }
272 
273         Map<String, String> metrics = mContext.getAttributes().getUniqueMap();
274         InvocationEndedEventInfo eventEnd = new InvocationEndedEventInfo(metrics);
275         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_ENDED, eventEnd);
276         // Upon invocation ended, trigger the end of the socket when the process finishes
277         SocketFinisher thread = new SocketFinisher();
278         Runtime.getRuntime().addShutdownHook(thread);
279     }
280 
281     /**
282      * {@inheritDoc}
283      */
284     @Override
invocationFailed(Throwable cause)285     public void invocationFailed(Throwable cause) {
286         InvocationFailedEventInfo info = new InvocationFailedEventInfo(cause);
287         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_FAILED, info);
288     }
289 
290     /** {@inheritDoc} */
291     @Override
invocationFailed(FailureDescription failure)292     public void invocationFailed(FailureDescription failure) {
293         InvocationFailedEventInfo info = new InvocationFailedEventInfo(failure);
294         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_FAILED, info);
295     }
296 
297     /** {@inheritDoc} */
298     @Override
testModuleStarted(IInvocationContext moduleContext)299     public void testModuleStarted(IInvocationContext moduleContext) {
300         TestModuleStartedEventInfo info = new TestModuleStartedEventInfo(moduleContext);
301         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_MODULE_STARTED, info);
302     }
303 
304     /** {@inheritDoc} */
305     @Override
testModuleEnded()306     public void testModuleEnded() {
307         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_MODULE_ENDED, new JSONObject());
308     }
309 
310     /**
311      * {@inheritDoc}
312      */
313     @Override
getSummary()314     public TestSummary getSummary() {
315         return null;
316     }
317 
318     /**
319      * Helper to print the event key and then the json object.
320      */
printEvent(String key, Object event)321     public void printEvent(String key, Object event) {
322         if (mReportFile != null) {
323             if (mReportFile.canWrite()) {
324                 try {
325                     try (FileWriter fw = new FileWriter(mReportFile, true)) {
326                         String eventLog = String.format("%s %s\n", key, event.toString());
327                         fw.append(eventLog);
328                         fw.flush();
329                     }
330                 } catch (IOException e) {
331                     throw new RuntimeException(e);
332                 }
333             } else {
334                 throw new RuntimeException(
335                         String.format("report file: %s is not writable",
336                                 mReportFile.getAbsolutePath()));
337             }
338         }
339         if(mReportPort != null) {
340             try {
341                 if (mCancelled) {
342                     return;
343                 }
344                 synchronized (mLock) {
345                     if (mReportSocket == null) {
346                         mReportSocket = new Socket("localhost", mReportPort.intValue());
347                         mPrintWriter = new PrintWriter(mReportSocket.getOutputStream(), true);
348                     }
349                     if (!mReportSocket.isConnected()) {
350                         throw new RuntimeException("Reporter Socket is not connected");
351                     }
352                     String eventLog = String.format("%s %s\n", key, event.toString());
353                     mPrintWriter.print(eventLog);
354                     mPrintWriter.flush();
355                 }
356             } catch (IOException e) {
357                 throw new RuntimeException(e);
358             }
359         }
360         if (mReportFile == null && mReportPort == null) {
361             if (mPrintWarning) {
362                 // Only print the warning the first time.
363                 mPrintWarning = false;
364                 CLog.w("No report file or socket has been configured, skipping this reporter.");
365             }
366         }
367     }
368 
369     /** {@inheritDoc} */
370     @Override
close()371     public void close() {
372         mCancelled = true;
373         synchronized (mLock) {
374             if (mPrintWriter != null) {
375                 mPrintWriter.flush();
376             }
377             StreamUtil.close(mPrintWriter);
378             mPrintWriter = null;
379             StreamUtil.close(mReportSocket);
380             mReportSocket = null;
381         }
382     }
383 
384     /** Sets whether or not we should output the test logged or not. */
setOutputTestLog(boolean outputTestLog)385     public void setOutputTestLog(boolean outputTestLog) {
386         mOutputTestlog = outputTestLog;
387     }
388 
389     /** Threads that help terminating the socket. */
390     private class SocketFinisher extends Thread {
391 
SocketFinisher()392         public SocketFinisher() {
393             super();
394             setName("SubprocessResultsReporter-socket-finisher");
395         }
396 
397         @Override
run()398         public void run() {
399             close();
400         }
401     }
402 }
403