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