• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.proto;
17 
18 import com.android.tradefed.build.IBuildInfo;
19 import com.android.tradefed.invoker.IInvocationContext;
20 import com.android.tradefed.invoker.InvocationContext;
21 import com.android.tradefed.invoker.proto.InvocationContext.Context;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
24 import com.android.tradefed.result.FileInputStreamSource;
25 import com.android.tradefed.result.ILogSaverListener;
26 import com.android.tradefed.result.ITestInvocationListener;
27 import com.android.tradefed.result.InputStreamSource;
28 import com.android.tradefed.result.LogDataType;
29 import com.android.tradefed.result.LogFile;
30 import com.android.tradefed.result.TestDescription;
31 import com.android.tradefed.result.proto.LogFileProto.LogFileInfo;
32 import com.android.tradefed.result.proto.TestRecordProto.ChildReference;
33 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
34 import com.android.tradefed.testtype.suite.ModuleDefinition;
35 
36 import com.google.common.base.Strings;
37 import com.google.protobuf.Any;
38 import com.google.protobuf.InvalidProtocolBufferException;
39 import com.google.protobuf.Timestamp;
40 
41 import java.io.File;
42 import java.lang.reflect.InvocationTargetException;
43 import java.lang.reflect.Method;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map.Entry;
47 
48 /** Parser for the Tradefed results proto format. */
49 public class ProtoResultParser {
50 
51     private ITestInvocationListener mListener;
52     private String mCurrentRunName = null;
53     /**
54      * We don't always want to report the invocation level events again. If we are within an
55      * invocation scope we should not report it again.
56      */
57     private boolean mReportInvocation = false;
58     /** Prefix that will be added to the files logged through the parser. */
59     private String mFilePrefix;
60     /** The context from the invocation in progress, not the proto one. */
61     private IInvocationContext mMainContext;
62 
63     private boolean mQuietParsing = true;
64 
65     /** Ctor. */
ProtoResultParser( ITestInvocationListener listener, IInvocationContext context, boolean reportInvocation)66     public ProtoResultParser(
67             ITestInvocationListener listener,
68             IInvocationContext context,
69             boolean reportInvocation) {
70         this(listener, context, reportInvocation, "subprocess-");
71     }
72 
73     /** Ctor. */
ProtoResultParser( ITestInvocationListener listener, IInvocationContext context, boolean reportInvocation, String prefixForFile)74     public ProtoResultParser(
75             ITestInvocationListener listener,
76             IInvocationContext context,
77             boolean reportInvocation,
78             String prefixForFile) {
79         mListener = listener;
80         mMainContext = context;
81         mReportInvocation = reportInvocation;
82         mFilePrefix = prefixForFile;
83     }
84 
85     /** Enumeration representing the current level of the proto being processed. */
86     public enum TestLevel {
87         INVOCATION,
88         MODULE,
89         TEST_RUN,
90         TEST_CASE
91     }
92 
93     /** Sets whether or not to print when events are received. */
setQuiet(boolean quiet)94     public void setQuiet(boolean quiet) {
95         mQuietParsing = quiet;
96     }
97 
98     /**
99      * Main entry function that takes the finalized completed proto and replay its results.
100      *
101      * @param finalProto The final {@link TestRecord} to be parsed.
102      */
processFinalizedProto(TestRecord finalProto)103     public void processFinalizedProto(TestRecord finalProto) {
104         if (!finalProto.getParentTestRecordId().isEmpty()) {
105             throw new IllegalArgumentException("processFinalizedProto only expect a root proto.");
106         }
107 
108         // Invocation Start
109         handleInvocationStart(finalProto);
110 
111         evalProto(finalProto.getChildrenList(), false);
112         // Invocation End
113         handleInvocationEnded(finalProto);
114     }
115 
116     /**
117      * Main entry function where each proto is presented to get parsed into Tradefed events.
118      *
119      * @param currentProto The current {@link TestRecord} to be parsed.
120      * @return True if the proto processed was a module.
121      */
processNewProto(TestRecord currentProto)122     public TestLevel processNewProto(TestRecord currentProto) {
123         // Handle initial root proto
124         if (currentProto.getParentTestRecordId().isEmpty()) {
125             handleRootProto(currentProto);
126             return TestLevel.INVOCATION;
127         } else if (currentProto.hasDescription()) {
128             // If it has a Any Description with Context then it's a module
129             handleModuleProto(currentProto);
130             return TestLevel.MODULE;
131         } else if (mCurrentRunName == null
132                 || currentProto.getTestRecordId().equals(mCurrentRunName)) {
133             // Need to track the parent test run id to make sure we need testRunEnd or testRunFail
134             handleTestRun(currentProto);
135             return TestLevel.TEST_RUN;
136         } else {
137             // Test cases handling
138             handleTestCase(currentProto);
139             return TestLevel.TEST_CASE;
140         }
141     }
142 
evalProto(List<ChildReference> children, boolean isInRun)143     private void evalProto(List<ChildReference> children, boolean isInRun) {
144         for (ChildReference child : children) {
145             TestRecord childProto = child.getInlineTestRecord();
146             if (isInRun) {
147                 // test case
148                 String[] info = childProto.getTestRecordId().split("#");
149                 TestDescription description = new TestDescription(info[0], info[1]);
150                 mListener.testStarted(description, timeStampToMillis(childProto.getStartTime()));
151                 handleTestCaseEnd(description, childProto);
152             } else {
153                 boolean inRun = false;
154                 if (childProto.hasDescription()) {
155                     // Module start
156                     handleModuleStart(childProto);
157                 } else {
158                     // run start
159                     handleTestRunStart(childProto);
160                     inRun = true;
161                 }
162                 evalProto(childProto.getChildrenList(), inRun);
163                 if (childProto.hasDescription()) {
164                     // Module end
165                     handleModuleProto(childProto);
166                 } else {
167                     // run end
168                     handleTestRunEnd(childProto);
169                 }
170             }
171         }
172     }
173 
174     /** Handles the root of the invocation: They have no parent record id. */
handleRootProto(TestRecord rootProto)175     private void handleRootProto(TestRecord rootProto) {
176         if (rootProto.hasEndTime()) {
177             handleInvocationEnded(rootProto);
178         } else {
179             handleInvocationStart(rootProto);
180         }
181     }
182 
handleInvocationStart(TestRecord startInvocationProto)183     private void handleInvocationStart(TestRecord startInvocationProto) {
184         // invocation starting
185         Any anyDescription = startInvocationProto.getDescription();
186         if (!anyDescription.is(Context.class)) {
187             throw new RuntimeException("Expected Any description of type Context");
188         }
189         IInvocationContext receivedProto;
190         try {
191             receivedProto = InvocationContext.fromProto(anyDescription.unpack(Context.class));
192         } catch (InvalidProtocolBufferException e) {
193             throw new RuntimeException(e);
194         }
195 
196         log("Invocation started proto");
197         if (!mReportInvocation) {
198             CLog.d("Skipping invocation start reporting.");
199             return;
200         }
201         // Only report invocation start if enabled
202         mListener.invocationStarted(receivedProto);
203     }
204 
handleInvocationEnded(TestRecord endInvocationProto)205     private void handleInvocationEnded(TestRecord endInvocationProto) {
206         // Still report the logs even if not reporting the invocation level.
207         handleLogs(endInvocationProto);
208 
209         // Get final context in case it changed.
210         Any anyDescription = endInvocationProto.getDescription();
211         if (!anyDescription.is(Context.class)) {
212             throw new RuntimeException("Expected Any description of type Context");
213         }
214         try {
215             IInvocationContext context =
216                     InvocationContext.fromProto(anyDescription.unpack(Context.class));
217             mergeInvocationContext(mMainContext, context);
218         } catch (InvalidProtocolBufferException e) {
219             throw new RuntimeException(e);
220         }
221 
222         if (endInvocationProto.hasDebugInfo()) {
223             // TODO: Re-interpret the exception with proper type.
224             String trace = endInvocationProto.getDebugInfo().getTrace();
225             mListener.invocationFailed(new Throwable(trace));
226         }
227 
228         log("Invocation ended proto");
229         if (!mReportInvocation) {
230             CLog.d("Skipping invocation ended reporting.");
231             return;
232         }
233         // Only report invocation ended if enabled
234         long elapsedTime =
235                 timeStampToMillis(endInvocationProto.getEndTime())
236                         - timeStampToMillis(endInvocationProto.getStartTime());
237         mListener.invocationEnded(elapsedTime);
238     }
239 
240     /** Handles module level of the invocation: They have a Description for the module context. */
handleModuleProto(TestRecord moduleProto)241     private void handleModuleProto(TestRecord moduleProto) {
242         if (moduleProto.hasEndTime()) {
243             handleModuleEnded(moduleProto);
244         } else {
245             handleModuleStart(moduleProto);
246         }
247     }
248 
handleModuleStart(TestRecord moduleProto)249     private void handleModuleStart(TestRecord moduleProto) {
250         Any anyDescription = moduleProto.getDescription();
251         if (!anyDescription.is(Context.class)) {
252             throw new RuntimeException("Expected Any description of type Context");
253         }
254         try {
255             IInvocationContext moduleContext =
256                     InvocationContext.fromProto(anyDescription.unpack(Context.class));
257             String message = "Test module started proto";
258             if (moduleContext.getAttributes().containsKey(ModuleDefinition.MODULE_ID)) {
259                 message +=
260                         (": "
261                                 + moduleContext
262                                         .getAttributes()
263                                         .getUniqueMap()
264                                         .get(ModuleDefinition.MODULE_ID));
265             }
266             log(message);
267             mListener.testModuleStarted(moduleContext);
268         } catch (InvalidProtocolBufferException e) {
269             throw new RuntimeException(e);
270         }
271     }
272 
handleModuleEnded(TestRecord moduleProto)273     private void handleModuleEnded(TestRecord moduleProto) {
274         handleLogs(moduleProto);
275         log("Test module ended proto");
276         mListener.testModuleEnded();
277     }
278 
279     /** Handles the test run level of the invocation. */
handleTestRun(TestRecord runProto)280     private void handleTestRun(TestRecord runProto) {
281         // If the proto end-time is present we are evaluating the end of a test run.
282         if (runProto.hasEndTime()) {
283             handleTestRunEnd(runProto);
284             mCurrentRunName = null;
285         } else {
286             // If the end-time is not populated yet we are dealing with the start of a run.
287             mCurrentRunName = runProto.getTestRecordId();
288             handleTestRunStart(runProto);
289         }
290     }
291 
handleTestRunStart(TestRecord runProto)292     private void handleTestRunStart(TestRecord runProto) {
293         String id = runProto.getTestRecordId();
294         log(
295                 "Test run started proto: %s. Expected tests: %s. Attempt: %s",
296                 id, runProto.getNumExpectedChildren(), runProto.getAttemptId());
297         mListener.testRunStarted(
298                 id,
299                 (int) runProto.getNumExpectedChildren(),
300                 (int) runProto.getAttemptId(),
301                 timeStampToMillis(runProto.getStartTime()));
302     }
303 
handleTestRunEnd(TestRecord runProto)304     private void handleTestRunEnd(TestRecord runProto) {
305         // If we find debugging information, the test run failed and we reflect it.
306         if (runProto.hasDebugInfo()) {
307             mListener.testRunFailed(runProto.getDebugInfo().getErrorMessage());
308         }
309         handleLogs(runProto);
310         log("Test run ended proto: %s", runProto.getTestRecordId());
311         long elapsedTime =
312                 timeStampToMillis(runProto.getEndTime())
313                         - timeStampToMillis(runProto.getStartTime());
314         HashMap<String, Metric> metrics = new HashMap<>(runProto.getMetrics());
315         mListener.testRunEnded(elapsedTime, metrics);
316     }
317 
318     /** Handles the test cases level of the invocation. */
handleTestCase(TestRecord testcaseProto)319     private void handleTestCase(TestRecord testcaseProto) {
320         String[] info = testcaseProto.getTestRecordId().split("#");
321         TestDescription description = new TestDescription(info[0], info[1]);
322         if (testcaseProto.hasEndTime()) {
323             handleTestCaseEnd(description, testcaseProto);
324         } else {
325             log("Test case started proto: %s", description.toString());
326             mListener.testStarted(description, timeStampToMillis(testcaseProto.getStartTime()));
327         }
328     }
329 
handleTestCaseEnd(TestDescription description, TestRecord testcaseProto)330     private void handleTestCaseEnd(TestDescription description, TestRecord testcaseProto) {
331         switch (testcaseProto.getStatus()) {
332             case FAIL:
333                 mListener.testFailed(description, testcaseProto.getDebugInfo().getErrorMessage());
334                 log(
335                         "Test case failed proto: %s - %s",
336                         description.toString(), testcaseProto.getDebugInfo().getErrorMessage());
337                 break;
338             case ASSUMPTION_FAILURE:
339                 mListener.testAssumptionFailure(
340                         description, testcaseProto.getDebugInfo().getTrace());
341                 log(
342                         "Test case assumption failure proto: %s - %s",
343                         description.toString(), testcaseProto.getDebugInfo().getTrace());
344                 break;
345             case IGNORED:
346                 mListener.testIgnored(description);
347                 log("Test case ignored proto: %s", description.toString());
348                 break;
349             case PASS:
350                 break;
351             default:
352                 throw new RuntimeException(
353                         String.format(
354                                 "Received unexpected test status %s.", testcaseProto.getStatus()));
355         }
356         handleLogs(testcaseProto);
357         HashMap<String, Metric> metrics = new HashMap<>(testcaseProto.getMetrics());
358         log("Test case ended proto: %s", description.toString());
359         mListener.testEnded(description, timeStampToMillis(testcaseProto.getEndTime()), metrics);
360     }
361 
timeStampToMillis(Timestamp stamp)362     private long timeStampToMillis(Timestamp stamp) {
363         return stamp.getSeconds() * 1000L + (stamp.getNanos() / 1000000L);
364     }
365 
handleLogs(TestRecord proto)366     private void handleLogs(TestRecord proto) {
367         if (!(mListener instanceof ILogSaverListener)) {
368             return;
369         }
370         ILogSaverListener logger = (ILogSaverListener) mListener;
371         for (Entry<String, Any> entry : proto.getArtifacts().entrySet()) {
372             try {
373                 LogFileInfo info = entry.getValue().unpack(LogFileInfo.class);
374                 LogFile file =
375                         new LogFile(
376                                 info.getPath(),
377                                 info.getUrl(),
378                                 info.getIsCompressed(),
379                                 LogDataType.valueOf(info.getLogType()),
380                                 info.getSize());
381                 if (file.getPath() == null) {
382                     CLog.e("Log '%s' was registered but without a path.", entry.getKey());
383                     return;
384                 }
385                 File path = new File(file.getPath());
386                 if (Strings.isNullOrEmpty(file.getUrl()) && path.exists()) {
387                     try (InputStreamSource source = new FileInputStreamSource(path)) {
388                         LogDataType type = file.getType();
389                         // File might have already been compressed
390                         if (file.getPath().endsWith(LogDataType.ZIP.getFileExt())) {
391                             type = LogDataType.ZIP;
392                         }
393                         log("Logging %s from subprocess: %s ", entry.getKey(), file.getPath());
394                         logger.testLog(mFilePrefix + entry.getKey(), type, source);
395                     }
396                 } else {
397                     log("Logging %s from subprocess: %s", entry.getKey(), file.getUrl());
398                     logger.logAssociation(mFilePrefix + entry.getKey(), file);
399                 }
400             } catch (InvalidProtocolBufferException e) {
401                 CLog.e("Couldn't unpack %s as a LogFileInfo", entry.getKey());
402                 CLog.e(e);
403             }
404         }
405     }
406 
407     /**
408      * Copy the build info and invocation attributes from the proto context to the current
409      * invocation context
410      *
411      * @param receiverContext The context receiving the attributes
412      * @param endInvocationContext The context providing the attributes
413      */
mergeInvocationContext( IInvocationContext receiverContext, IInvocationContext endInvocationContext)414     private void mergeInvocationContext(
415             IInvocationContext receiverContext, IInvocationContext endInvocationContext) {
416         if (receiverContext == null) {
417             return;
418         }
419         // Gather attributes of build infos
420         for (IBuildInfo info : receiverContext.getBuildInfos()) {
421             String name = receiverContext.getBuildInfoName(info);
422             IBuildInfo endInvocationInfo = endInvocationContext.getBuildInfo(name);
423             if (endInvocationInfo == null) {
424                 CLog.e("No build info named: %s", name);
425                 continue;
426             }
427             info.addBuildAttributes(endInvocationInfo.getBuildAttributes());
428         }
429 
430         try {
431             Method unlock = InvocationContext.class.getDeclaredMethod("unlock");
432             unlock.setAccessible(true);
433             unlock.invoke(receiverContext);
434             unlock.setAccessible(false);
435         } catch (NoSuchMethodException
436                 | SecurityException
437                 | IllegalAccessException
438                 | IllegalArgumentException
439                 | InvocationTargetException e) {
440             CLog.e("Couldn't unlock the main context. Skip copying attributes");
441             return;
442         }
443         // Copy invocation attributes
444         receiverContext.addInvocationAttributes(endInvocationContext.getAttributes());
445     }
446 
log(String format, Object... obj)447     private void log(String format, Object... obj) {
448         if (!mQuietParsing) {
449             CLog.d(format, obj);
450         }
451     }
452 }
453