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.logger.InvocationMetricLogger;
22 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
23 import com.android.tradefed.invoker.logger.TfObjectTracker;
24 import com.android.tradefed.invoker.proto.InvocationContext.Context;
25 import com.android.tradefed.log.LogUtil.CLog;
26 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
27 import com.android.tradefed.result.ActionInProgress;
28 import com.android.tradefed.result.FailureDescription;
29 import com.android.tradefed.result.FileInputStreamSource;
30 import com.android.tradefed.result.ILogSaverListener;
31 import com.android.tradefed.result.ITestInvocationListener;
32 import com.android.tradefed.result.InputStreamSource;
33 import com.android.tradefed.result.LogDataType;
34 import com.android.tradefed.result.LogFile;
35 import com.android.tradefed.result.TestDescription;
36 import com.android.tradefed.result.error.ErrorIdentifier;
37 import com.android.tradefed.result.proto.LogFileProto.LogFileInfo;
38 import com.android.tradefed.result.proto.TestRecordProto.ChildReference;
39 import com.android.tradefed.result.proto.TestRecordProto.DebugInfo;
40 import com.android.tradefed.result.proto.TestRecordProto.DebugInfoContext;
41 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
42 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
43 import com.android.tradefed.testtype.suite.ModuleDefinition;
44 import com.android.tradefed.util.MultiMap;
45 import com.android.tradefed.util.SerializationUtil;
46 import com.android.tradefed.util.proto.TestRecordProtoUtil;
47 
48 import com.google.common.base.Splitter;
49 import com.google.common.base.Strings;
50 import com.google.protobuf.Any;
51 import com.google.protobuf.InvalidProtocolBufferException;
52 import com.google.protobuf.Timestamp;
53 
54 import java.io.File;
55 import java.io.IOException;
56 import java.lang.reflect.InvocationTargetException;
57 import java.lang.reflect.Method;
58 import java.util.HashMap;
59 import java.util.List;
60 import java.util.Map.Entry;
61 
62 /** Parser for the Tradefed results proto format. */
63 public class ProtoResultParser {
64 
65     private ITestInvocationListener mListener;
66     private String mCurrentRunName = null;
67     /**
68      * We don't always want to report the invocation level events again. If we are within an
69      * invocation scope we should not report it again.
70      */
71     private boolean mReportInvocation = false;
72     /** Prefix that will be added to the files logged through the parser. */
73     private String mFilePrefix;
74     /** The context from the invocation in progress, not the proto one. */
75     private IInvocationContext mMainContext;
76 
77     private boolean mQuietParsing = true;
78 
79     private boolean mInvocationStarted = false;
80     private boolean mInvocationEnded = false;
81     private boolean mFirstModule = true;
82     /** Track the name of the module in progress. */
83     private String mModuleInProgress = null;
84 
85     /** Ctor. */
ProtoResultParser( ITestInvocationListener listener, IInvocationContext context, boolean reportInvocation)86     public ProtoResultParser(
87             ITestInvocationListener listener,
88             IInvocationContext context,
89             boolean reportInvocation) {
90         this(listener, context, reportInvocation, "subprocess-");
91     }
92 
93     /** Ctor. */
ProtoResultParser( ITestInvocationListener listener, IInvocationContext context, boolean reportInvocation, String prefixForFile)94     public ProtoResultParser(
95             ITestInvocationListener listener,
96             IInvocationContext context,
97             boolean reportInvocation,
98             String prefixForFile) {
99         mListener = listener;
100         mMainContext = context;
101         mReportInvocation = reportInvocation;
102         mFilePrefix = prefixForFile;
103     }
104 
105     /** Enumeration representing the current level of the proto being processed. */
106     public enum TestLevel {
107         INVOCATION,
108         MODULE,
109         TEST_RUN,
110         TEST_CASE
111     }
112 
113     /** Sets whether or not to print when events are received. */
setQuiet(boolean quiet)114     public void setQuiet(boolean quiet) {
115         mQuietParsing = quiet;
116     }
117 
118     /**
119      * Main entry function that takes the finalized completed proto and replay its results.
120      *
121      * @param finalProto The final {@link TestRecord} to be parsed.
122      */
processFinalizedProto(TestRecord finalProto)123     public void processFinalizedProto(TestRecord finalProto) {
124         if (!finalProto.getParentTestRecordId().isEmpty()) {
125             throw new IllegalArgumentException("processFinalizedProto only expect a root proto.");
126         }
127 
128         // Invocation Start
129         handleInvocationStart(finalProto);
130 
131         evalChildrenProto(finalProto.getChildrenList(), false);
132         // Invocation End
133         handleInvocationEnded(finalProto);
134     }
135 
136     /**
137      * Main entry function where each proto is presented to get parsed into Tradefed events.
138      *
139      * @param currentProto The current {@link TestRecord} to be parsed.
140      * @return True if the proto processed was a module.
141      */
processNewProto(TestRecord currentProto)142     public TestLevel processNewProto(TestRecord currentProto) {
143         // Handle initial root proto
144         if (currentProto.getParentTestRecordId().isEmpty()) {
145             handleRootProto(currentProto);
146             return TestLevel.INVOCATION;
147         } else if (currentProto.hasDescription()) {
148             // If it has a Any Description with Context then it's a module
149             handleModuleProto(currentProto);
150             return TestLevel.MODULE;
151         } else if (mCurrentRunName == null
152                 || currentProto.getTestRecordId().equals(mCurrentRunName)) {
153             // Need to track the parent test run id to make sure we need testRunEnd or testRunFail
154             handleTestRun(currentProto);
155             return TestLevel.TEST_RUN;
156         } else {
157             // Test cases handling
158             handleTestCase(currentProto);
159             return TestLevel.TEST_CASE;
160         }
161     }
162 
163     /**
164      * In case of parsing proto files directly, handle direct parsing of them as a sequence.
165      * Associated with {@link FileProtoResultReporter} when reporting a sequence of files.
166      *
167      * @param protoFile The proto file to be parsed.
168      * @throws IOException
169      */
processFileProto(File protoFile)170     public void processFileProto(File protoFile) throws IOException {
171         TestRecord record = null;
172         try {
173             record = TestRecordProtoUtil.readFromFile(protoFile);
174         } catch (InvalidProtocolBufferException e) {
175             // Log the proto that failed to parse
176             try (FileInputStreamSource protoFail = new FileInputStreamSource(protoFile, true)) {
177                 mListener.testLog("failed-result-protobuf", LogDataType.PB, protoFail);
178             }
179             throw e;
180         }
181         if (!mInvocationStarted) {
182             handleInvocationStart(record);
183             mInvocationStarted = true;
184         } else if (record.getParentTestRecordId().isEmpty()) {
185             handleInvocationEnded(record);
186         } else {
187             evalProto(record, false);
188         }
189     }
190 
191     /** Returns whether or not the parsing reached an invocation ended. */
invocationEndedReached()192     public boolean invocationEndedReached() {
193         return mInvocationEnded;
194     }
195 
196     /** Returns the id of the module in progress. Returns null if none in progress. */
getModuleInProgress()197     public String getModuleInProgress() {
198         return mModuleInProgress;
199     }
200 
201     /** If needed to ensure consistent reporting, complete the events of the module. */
completeModuleEvents()202     public void completeModuleEvents() {
203         if (getModuleInProgress() == null) {
204             return;
205         }
206         mListener.testRunStarted(getModuleInProgress(), 0);
207         FailureDescription failure =
208                 FailureDescription.create(
209                         "Module was interrupted after starting, results are incomplete.",
210                         FailureStatus.INFRA_FAILURE);
211         mListener.testRunFailed(failure);
212         mListener.testRunEnded(0L, new HashMap<String, Metric>());
213         mListener.testModuleEnded();
214     }
215 
evalChildrenProto(List<ChildReference> children, boolean isInRun)216     private void evalChildrenProto(List<ChildReference> children, boolean isInRun) {
217         for (ChildReference child : children) {
218             TestRecord childProto = child.getInlineTestRecord();
219             evalProto(childProto, isInRun);
220         }
221     }
222 
evalProto(TestRecord childProto, boolean isInRun)223     private void evalProto(TestRecord childProto, boolean isInRun) {
224         if (isInRun) {
225             // test case
226             String[] info = childProto.getTestRecordId().split("#");
227             TestDescription description = new TestDescription(info[0], info[1]);
228             mListener.testStarted(description, timeStampToMillis(childProto.getStartTime()));
229             handleTestCaseEnd(description, childProto);
230         } else {
231             boolean inRun = false;
232             if (childProto.hasDescription()) {
233                 // Module start
234                 handleModuleStart(childProto);
235             } else {
236                 // run start
237                 handleTestRunStart(childProto);
238                 inRun = true;
239             }
240             evalChildrenProto(childProto.getChildrenList(), inRun);
241             if (childProto.hasDescription()) {
242                 // Module end
243                 handleModuleProto(childProto);
244             } else {
245                 // run end
246                 handleTestRunEnd(childProto);
247             }
248         }
249     }
250 
251     /** Handles the root of the invocation: They have no parent record id. */
handleRootProto(TestRecord rootProto)252     private void handleRootProto(TestRecord rootProto) {
253         if (rootProto.hasEndTime()) {
254             handleInvocationEnded(rootProto);
255         } else {
256             handleInvocationStart(rootProto);
257         }
258     }
259 
handleInvocationStart(TestRecord startInvocationProto)260     private void handleInvocationStart(TestRecord startInvocationProto) {
261         // invocation starting
262         Any anyDescription = startInvocationProto.getDescription();
263         if (!anyDescription.is(Context.class)) {
264             throw new RuntimeException("Expected Any description of type Context");
265         }
266         IInvocationContext receivedContext;
267         try {
268             receivedContext = InvocationContext.fromProto(anyDescription.unpack(Context.class));
269             mergeInvocationContext(mMainContext, receivedContext);
270         } catch (InvalidProtocolBufferException e) {
271             throw new RuntimeException(e);
272         }
273 
274         log("Invocation started proto");
275         if (!mReportInvocation) {
276             CLog.d("Skipping invocation start reporting.");
277             return;
278         }
279         // Only report invocation start if enabled
280         mListener.invocationStarted(receivedContext);
281     }
282 
handleInvocationEnded(TestRecord endInvocationProto)283     private void handleInvocationEnded(TestRecord endInvocationProto) {
284         // Still report the logs even if not reporting the invocation level.
285         handleLogs(endInvocationProto);
286 
287         // Get final context in case it changed.
288         Any anyDescription = endInvocationProto.getDescription();
289         if (!anyDescription.is(Context.class)) {
290             throw new RuntimeException(
291                     String.format(
292                             "Expected Any description of type Context, was %s", anyDescription));
293         }
294         try {
295             IInvocationContext context =
296                     InvocationContext.fromProto(anyDescription.unpack(Context.class));
297             mergeInvocationContext(mMainContext, context);
298         } catch (InvalidProtocolBufferException e) {
299             throw new RuntimeException(e);
300         }
301 
302         if (endInvocationProto.hasDebugInfo()) {
303             DebugInfo debugInfo = endInvocationProto.getDebugInfo();
304             FailureDescription failure = FailureDescription.create(debugInfo.getErrorMessage());
305             if (!TestRecordProto.FailureStatus.UNSET.equals(
306                     endInvocationProto.getDebugInfo().getFailureStatus())) {
307                 failure.setFailureStatus(debugInfo.getFailureStatus());
308             }
309             parseDebugInfoContext(endInvocationProto.getDebugInfo(), failure);
310             if (endInvocationProto.getDebugInfo().hasDebugInfoContext()) {
311                 String errorType =
312                         endInvocationProto.getDebugInfo().getDebugInfoContext().getErrorType();
313                 if (!Strings.isNullOrEmpty(errorType)) {
314                     try {
315                         Throwable invocationError =
316                                 (Throwable) SerializationUtil.deserialize(errorType);
317                         failure.setCause(invocationError);
318                     } catch (IOException e) {
319                         CLog.e("Failed to deserialize the invocation exception:");
320                         CLog.e(e);
321                     }
322                 }
323             }
324             mListener.invocationFailed(failure);
325         }
326 
327         log("Invocation ended proto");
328         mInvocationEnded = true;
329         if (!mReportInvocation) {
330             CLog.d("Skipping invocation ended reporting.");
331             return;
332         }
333         // Only report invocation ended if enabled
334         long elapsedTime =
335                 timeStampToMillis(endInvocationProto.getEndTime())
336                         - timeStampToMillis(endInvocationProto.getStartTime());
337         mListener.invocationEnded(elapsedTime);
338     }
339 
340     /** Handles module level of the invocation: They have a Description for the module context. */
handleModuleProto(TestRecord moduleProto)341     private void handleModuleProto(TestRecord moduleProto) {
342         if (moduleProto.hasEndTime()) {
343             handleModuleEnded(moduleProto);
344         } else {
345             handleModuleStart(moduleProto);
346         }
347     }
348 
handleModuleStart(TestRecord moduleProto)349     private void handleModuleStart(TestRecord moduleProto) {
350         Any anyDescription = moduleProto.getDescription();
351         if (!anyDescription.is(Context.class)) {
352             throw new RuntimeException("Expected Any description of type Context");
353         }
354         try {
355             IInvocationContext moduleContext =
356                     InvocationContext.fromProto(anyDescription.unpack(Context.class));
357             String message = "Test module started proto";
358             if (moduleContext.getAttributes().containsKey(ModuleDefinition.MODULE_ID)) {
359                 String moduleId =
360                         moduleContext
361                                 .getAttributes()
362                                 .getUniqueMap()
363                                 .get(ModuleDefinition.MODULE_ID);
364                 message += (": " + moduleId);
365                 mModuleInProgress = moduleId;
366             }
367             log(message);
368             mListener.testModuleStarted(moduleContext);
369             if (mFirstModule) {
370                 mFirstModule = false;
371                 // Parse the build attributes once after invocation start to update the BuildInfo
372                 mergeBuildInfo(mMainContext, moduleContext);
373             }
374         } catch (InvalidProtocolBufferException e) {
375             throw new RuntimeException(e);
376         }
377     }
378 
handleModuleEnded(TestRecord moduleProto)379     private void handleModuleEnded(TestRecord moduleProto) {
380         handleLogs(moduleProto);
381         log("Test module ended proto");
382         mListener.testModuleEnded();
383         mModuleInProgress = null;
384     }
385 
386     /** Handles the test run level of the invocation. */
handleTestRun(TestRecord runProto)387     private void handleTestRun(TestRecord runProto) {
388         // If the proto end-time is present we are evaluating the end of a test run.
389         if (runProto.hasEndTime()) {
390             handleTestRunEnd(runProto);
391             mCurrentRunName = null;
392         } else {
393             // If the end-time is not populated yet we are dealing with the start of a run.
394             mCurrentRunName = runProto.getTestRecordId();
395             handleTestRunStart(runProto);
396         }
397     }
398 
handleTestRunStart(TestRecord runProto)399     private void handleTestRunStart(TestRecord runProto) {
400         String id = runProto.getTestRecordId();
401         log(
402                 "Test run started proto: %s. Expected tests: %s. Attempt: %s",
403                 id, runProto.getNumExpectedChildren(), runProto.getAttemptId());
404         mListener.testRunStarted(
405                 id,
406                 (int) runProto.getNumExpectedChildren(),
407                 (int) runProto.getAttemptId(),
408                 timeStampToMillis(runProto.getStartTime()));
409     }
410 
handleTestRunEnd(TestRecord runProto)411     private void handleTestRunEnd(TestRecord runProto) {
412         // If we find debugging information, the test run failed and we reflect it.
413         if (runProto.hasDebugInfo()) {
414             DebugInfo debugInfo = runProto.getDebugInfo();
415             FailureDescription failure = FailureDescription.create(debugInfo.getErrorMessage());
416             if (!TestRecordProto.FailureStatus.UNSET.equals(
417                     runProto.getDebugInfo().getFailureStatus())) {
418                 failure.setFailureStatus(debugInfo.getFailureStatus());
419             }
420 
421             parseDebugInfoContext(debugInfo, failure);
422 
423             mListener.testRunFailed(failure);
424             log("Test run failure proto: %s", failure.toString());
425         }
426         handleLogs(runProto);
427         log("Test run ended proto: %s", runProto.getTestRecordId());
428         long elapsedTime =
429                 timeStampToMillis(runProto.getEndTime())
430                         - timeStampToMillis(runProto.getStartTime());
431         HashMap<String, Metric> metrics = new HashMap<>(runProto.getMetricsMap());
432         mListener.testRunEnded(elapsedTime, metrics);
433     }
434 
435     /** Handles the test cases level of the invocation. */
handleTestCase(TestRecord testcaseProto)436     private void handleTestCase(TestRecord testcaseProto) {
437         String[] info = testcaseProto.getTestRecordId().split("#");
438         TestDescription description = new TestDescription(info[0], info[1]);
439         if (testcaseProto.hasEndTime()) {
440             handleTestCaseEnd(description, testcaseProto);
441         } else {
442             log("Test case started proto: %s", description.toString());
443             mListener.testStarted(description, timeStampToMillis(testcaseProto.getStartTime()));
444         }
445     }
446 
handleTestCaseEnd(TestDescription description, TestRecord testcaseProto)447     private void handleTestCaseEnd(TestDescription description, TestRecord testcaseProto) {
448         DebugInfo debugInfo = testcaseProto.getDebugInfo();
449         switch (testcaseProto.getStatus()) {
450             case FAIL:
451                 FailureDescription failure =
452                         FailureDescription.create(testcaseProto.getDebugInfo().getErrorMessage());
453                 if (!TestRecordProto.FailureStatus.UNSET.equals(
454                         testcaseProto.getDebugInfo().getFailureStatus())) {
455                     failure.setFailureStatus(testcaseProto.getDebugInfo().getFailureStatus());
456                 }
457 
458                 parseDebugInfoContext(debugInfo, failure);
459 
460                 mListener.testFailed(description, failure);
461                 log("Test case failed proto: %s - %s", description.toString(), failure.toString());
462                 break;
463             case ASSUMPTION_FAILURE:
464                 FailureDescription assumption =
465                         FailureDescription.create(testcaseProto.getDebugInfo().getErrorMessage());
466                 if (!TestRecordProto.FailureStatus.UNSET.equals(
467                         testcaseProto.getDebugInfo().getFailureStatus())) {
468                     assumption.setFailureStatus(testcaseProto.getDebugInfo().getFailureStatus());
469                 }
470 
471                 parseDebugInfoContext(debugInfo, assumption);
472 
473                 mListener.testAssumptionFailure(description, assumption);
474                 log(
475                         "Test case assumption failure proto: %s - %s",
476                         description.toString(), testcaseProto.getDebugInfo().getTrace());
477                 break;
478             case IGNORED:
479                 mListener.testIgnored(description);
480                 log("Test case ignored proto: %s", description.toString());
481                 break;
482             case PASS:
483                 break;
484             default:
485                 throw new RuntimeException(
486                         String.format(
487                                 "Received unexpected test status %s.", testcaseProto.getStatus()));
488         }
489         handleLogs(testcaseProto);
490         HashMap<String, Metric> metrics = new HashMap<>(testcaseProto.getMetricsMap());
491         log("Test case ended proto: %s", description.toString());
492         mListener.testEnded(description, timeStampToMillis(testcaseProto.getEndTime()), metrics);
493     }
494 
timeStampToMillis(Timestamp stamp)495     private long timeStampToMillis(Timestamp stamp) {
496         return stamp.getSeconds() * 1000L + (stamp.getNanos() / 1000000L);
497     }
498 
handleLogs(TestRecord proto)499     private void handleLogs(TestRecord proto) {
500         if (!(mListener instanceof ILogSaverListener)) {
501             return;
502         }
503         ILogSaverListener logger = (ILogSaverListener) mListener;
504         for (Entry<String, Any> entry : proto.getArtifactsMap().entrySet()) {
505             try {
506                 LogFileInfo info = entry.getValue().unpack(LogFileInfo.class);
507                 LogFile file =
508                         new LogFile(
509                                 info.getPath(),
510                                 info.getUrl(),
511                                 info.getIsCompressed(),
512                                 LogDataType.valueOf(info.getLogType()),
513                                 info.getSize());
514                 if (Strings.isNullOrEmpty(file.getPath())) {
515                     CLog.e("Log '%s' was registered but without a path.", entry.getKey());
516                     return;
517                 }
518                 File path = new File(file.getPath());
519                 if (Strings.isNullOrEmpty(file.getUrl()) && path.exists()) {
520                     try (InputStreamSource source = new FileInputStreamSource(path)) {
521                         LogDataType type = file.getType();
522                         // File might have already been compressed
523                         if (file.getPath().endsWith(LogDataType.ZIP.getFileExt())) {
524                             type = LogDataType.ZIP;
525                         }
526                         log("Logging %s from subprocess: %s ", entry.getKey(), file.getPath());
527                         logger.testLog(mFilePrefix + entry.getKey(), type, source);
528                     }
529                 } else {
530                     log(
531                             "Logging %s from subprocess. url: %s, path: %s",
532                             entry.getKey(), file.getUrl(), file.getPath());
533                     logger.logAssociation(mFilePrefix + entry.getKey(), file);
534                 }
535             } catch (InvalidProtocolBufferException e) {
536                 CLog.e("Couldn't unpack %s as a LogFileInfo", entry.getKey());
537                 CLog.e(e);
538             }
539         }
540     }
541 
mergeBuildInfo( IInvocationContext receiverContext, IInvocationContext endInvocationContext)542     private void mergeBuildInfo(
543             IInvocationContext receiverContext, IInvocationContext endInvocationContext) {
544         if (receiverContext == null) {
545             return;
546         }
547         // Gather attributes of build infos
548         for (IBuildInfo info : receiverContext.getBuildInfos()) {
549             String name = receiverContext.getBuildInfoName(info);
550             IBuildInfo endInvocationInfo = endInvocationContext.getBuildInfo(name);
551             if (endInvocationInfo == null) {
552                 CLog.e("No build info named: %s", name);
553                 continue;
554             }
555             info.addBuildAttributes(endInvocationInfo.getBuildAttributes());
556         }
557     }
558 
559     /**
560      * Copy the build info and invocation attributes from the proto context to the current
561      * invocation context
562      *
563      * @param receiverContext The context receiving the attributes
564      * @param endInvocationContext The context providing the attributes
565      */
mergeInvocationContext( IInvocationContext receiverContext, IInvocationContext endInvocationContext)566     private void mergeInvocationContext(
567             IInvocationContext receiverContext, IInvocationContext endInvocationContext) {
568         if (receiverContext == null) {
569             return;
570         }
571         mergeBuildInfo(receiverContext, endInvocationContext);
572 
573         try {
574             Method unlock = InvocationContext.class.getDeclaredMethod("unlock");
575             unlock.setAccessible(true);
576             unlock.invoke(receiverContext);
577             unlock.setAccessible(false);
578         } catch (NoSuchMethodException
579                 | SecurityException
580                 | IllegalAccessException
581                 | IllegalArgumentException
582                 | InvocationTargetException e) {
583             CLog.e("Couldn't unlock the main context. Skip copying attributes");
584             return;
585         }
586         // Copy invocation attributes
587         MultiMap<String, String> attributes = endInvocationContext.getAttributes();
588         for (InvocationMetricKey key : InvocationMetricKey.values()) {
589             if (!attributes.containsKey(key.toString())) {
590                 continue;
591             }
592             List<String> values = attributes.get(key.toString());
593             attributes.remove(key.toString());
594 
595             for (String val : values) {
596                 if (key.shouldAdd()) {
597                     try {
598                         InvocationMetricLogger.addInvocationMetrics(key, Long.parseLong(val));
599                     } catch (NumberFormatException e) {
600                         CLog.d("Key %s doesn't have a number value, was: %s.", key, val);
601                         InvocationMetricLogger.addInvocationMetrics(key, val);
602                     }
603                 } else {
604                     InvocationMetricLogger.addInvocationMetrics(key, val);
605                 }
606             }
607         }
608         if (attributes.containsKey(TfObjectTracker.TF_OBJECTS_TRACKING_KEY)) {
609             List<String> values = attributes.get(TfObjectTracker.TF_OBJECTS_TRACKING_KEY);
610             for (String val : values) {
611                 for (String pair : Splitter.on(",").split(val)) {
612                     if (!pair.contains("=")) {
613                         continue;
614                     }
615                     String[] pairSplit = pair.split("=");
616                     try {
617                         TfObjectTracker.directCount(pairSplit[0], Long.parseLong(pairSplit[1]));
618                     } catch (NumberFormatException e) {
619                         CLog.e(e);
620                         continue;
621                     }
622                 }
623             }
624             attributes.remove(TfObjectTracker.TF_OBJECTS_TRACKING_KEY);
625         }
626         receiverContext.addInvocationAttributes(attributes);
627     }
628 
log(String format, Object... obj)629     private void log(String format, Object... obj) {
630         if (!mQuietParsing) {
631             CLog.d(format, obj);
632         }
633     }
634 
parseDebugInfoContext(DebugInfo debugInfo, FailureDescription failure)635     private void parseDebugInfoContext(DebugInfo debugInfo, FailureDescription failure) {
636         if (!debugInfo.hasDebugInfoContext()) {
637             return;
638         }
639         DebugInfoContext debugContext = debugInfo.getDebugInfoContext();
640         if (!Strings.isNullOrEmpty(debugContext.getActionInProgress())) {
641             try {
642                 ActionInProgress value =
643                         ActionInProgress.valueOf(debugContext.getActionInProgress());
644                 failure.setActionInProgress(value);
645             } catch (IllegalArgumentException parseError) {
646                 CLog.e(parseError);
647             }
648         }
649         if (!Strings.isNullOrEmpty(debugContext.getDebugHelpMessage())) {
650             failure.setDebugHelpMessage(debugContext.getDebugHelpMessage());
651         }
652         if (!Strings.isNullOrEmpty(debugContext.getOrigin())) {
653             failure.setOrigin(debugContext.getOrigin());
654         }
655         String errorName = debugContext.getErrorName();
656         long errorCode = debugContext.getErrorCode();
657         if (!Strings.isNullOrEmpty(errorName)) {
658             // Most of the implementations will be Enums which represent the name/code.
659             // But since there might be several Enum implementation of ErrorIdentifier, we can't
660             // parse back the name to the Enum so instead we stub create a pre-populated
661             // ErrorIdentifier to carry the infos.
662             ErrorIdentifier errorId =
663                     new ErrorIdentifier() {
664                         @Override
665                         public String name() {
666                             return errorName;
667                         }
668 
669                         @Override
670                         public long code() {
671                             return errorCode;
672                         }
673 
674                         @Override
675                         public FailureStatus status() {
676                             return failure.getFailureStatus();
677                         }
678                     };
679             failure.setErrorIdentifier(errorId);
680         }
681     }
682 }
683