1 /* 2 * Copyright (C) 2015 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.compatibility.common.tradefed.result; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.tradefed.testtype.CompatibilityTest; 20 import com.android.compatibility.common.util.ICaseResult; 21 import com.android.compatibility.common.util.IInvocationResult; 22 import com.android.compatibility.common.util.IModuleResult; 23 import com.android.compatibility.common.util.ITestResult; 24 import com.android.compatibility.common.util.InvocationResult; 25 import com.android.compatibility.common.util.MetricsStore; 26 import com.android.compatibility.common.util.ReportLog; 27 import com.android.compatibility.common.util.ResultHandler; 28 import com.android.compatibility.common.util.ResultUploader; 29 import com.android.compatibility.common.util.TestStatus; 30 import com.android.ddmlib.Log; 31 import com.android.ddmlib.Log.LogLevel; 32 import com.android.ddmlib.testrunner.TestIdentifier; 33 import com.android.tradefed.build.IBuildInfo; 34 import com.android.tradefed.config.Option; 35 import com.android.tradefed.config.Option.Importance; 36 import com.android.tradefed.config.OptionClass; 37 import com.android.tradefed.config.OptionCopier; 38 import com.android.tradefed.log.LogUtil.CLog; 39 import com.android.tradefed.result.ILogSaver; 40 import com.android.tradefed.result.ILogSaverListener; 41 import com.android.tradefed.result.IShardableListener; 42 import com.android.tradefed.result.ITestInvocationListener; 43 import com.android.tradefed.result.ITestSummaryListener; 44 import com.android.tradefed.result.InputStreamSource; 45 import com.android.tradefed.result.LogDataType; 46 import com.android.tradefed.result.LogFile; 47 import com.android.tradefed.result.LogFileSaver; 48 import com.android.tradefed.result.TestSummary; 49 import com.android.tradefed.util.FileUtil; 50 import com.android.tradefed.util.StreamUtil; 51 import com.android.tradefed.util.TimeUtil; 52 import com.android.tradefed.util.ZipUtil; 53 54 import org.xmlpull.v1.XmlPullParserException; 55 56 import java.io.File; 57 import java.io.FileInputStream; 58 import java.io.FileNotFoundException; 59 import java.io.IOException; 60 import java.io.InputStream; 61 import java.text.SimpleDateFormat; 62 import java.util.Collections; 63 import java.util.Date; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.Map.Entry; 68 import java.util.Set; 69 70 /** 71 * Collect test results for an entire invocation and output test results to disk. 72 */ 73 @OptionClass(alias="result-reporter") 74 public class ResultReporter implements ILogSaverListener, ITestInvocationListener, 75 ITestSummaryListener, IShardableListener { 76 77 private static final String UNKNOWN_DEVICE = "unknown_device"; 78 private static final String RESULT_KEY = "COMPATIBILITY_TEST_RESULT"; 79 private static final String CTS_PREFIX = "cts:"; 80 private static final String BUILD_INFO = CTS_PREFIX + "build_"; 81 private static final String[] RESULT_RESOURCES = { 82 "compatibility_result.css", 83 "compatibility_result.xsd", 84 "compatibility_result.xsl", 85 "logo.png"}; 86 87 @Option(name = CompatibilityTest.RETRY_OPTION, 88 shortName = 'r', 89 description = "retry a previous session.", 90 importance = Importance.IF_UNSET) 91 private Integer mRetrySessionId = null; 92 93 @Option(name = "result-server", description = "Server to publish test results.") 94 private String mResultServer; 95 96 @Option(name = "disable-result-posting", description = "Disable result posting into report server.") 97 private boolean mDisableResultPosting = false; 98 99 @Option(name = "include-test-log-tags", description = "Include test log tags in report.") 100 private boolean mIncludeTestLogTags = false; 101 102 @Option(name = "use-log-saver", description = "Also saves generated result with log saver") 103 private boolean mUseLogSaver = false; 104 105 private CompatibilityBuildHelper mBuildHelper; 106 private File mResultDir = null; 107 private File mLogDir = null; 108 private ResultUploader mUploader; 109 private String mReferenceUrl; 110 private ILogSaver mLogSaver; 111 private int invocationEndedCount = 0; 112 113 private IInvocationResult mResult = new InvocationResult(); 114 private IModuleResult mCurrentModuleResult; 115 private ICaseResult mCurrentCaseResult; 116 private ITestResult mCurrentResult; 117 private String mDeviceSerial = UNKNOWN_DEVICE; 118 private Set<String> mMasterDeviceSerials = new HashSet<>(); 119 private Set<IBuildInfo> mMasterBuildInfos = new HashSet<>(); 120 121 // mCurrentTestNum and mTotalTestsInModule track the progress within the module 122 // Note that this count is not necessarily equal to the count of tests contained 123 // in mCurrentModuleResult because of how special cases like ignored tests are reported. 124 private int mCurrentTestNum; 125 private int mTotalTestsInModule; 126 127 // Nullable. If null, "this" is considered the master and must handle 128 // result aggregation and reporting. When not null, it should forward events 129 // to the master. 130 private final ResultReporter mMasterResultReporter; 131 132 /** 133 * Default constructor. 134 */ ResultReporter()135 public ResultReporter() { 136 this(null); 137 } 138 139 /** 140 * Construct a shard ResultReporter that forwards module results to the 141 * masterResultReporter. 142 */ ResultReporter(ResultReporter masterResultReporter)143 public ResultReporter(ResultReporter masterResultReporter) { 144 mMasterResultReporter = masterResultReporter; 145 } 146 147 /** 148 * {@inheritDoc} 149 */ 150 @Override invocationStarted(IBuildInfo buildInfo)151 public void invocationStarted(IBuildInfo buildInfo) { 152 synchronized(this) { 153 if (mBuildHelper == null) { 154 mBuildHelper = new CompatibilityBuildHelper(buildInfo); 155 } 156 if (mDeviceSerial == null && buildInfo.getDeviceSerial() != null) { 157 mDeviceSerial = buildInfo.getDeviceSerial(); 158 } 159 } 160 161 if (isShardResultReporter()) { 162 // Shard ResultReporters forward invocationStarted to the mMasterResultReporter 163 mMasterResultReporter.invocationStarted(buildInfo); 164 return; 165 } 166 167 // NOTE: Everything after this line only applies to the master ResultReporter. 168 169 synchronized(this) { 170 if (buildInfo.getDeviceSerial() != null) { 171 // The master ResultReporter collects all device serials being used 172 // for the current implementation. 173 mMasterDeviceSerials.add(buildInfo.getDeviceSerial()); 174 } 175 176 // The master ResultReporter collects all buildInfos. 177 mMasterBuildInfos.add(buildInfo); 178 179 if (mResultDir == null) { 180 // For the non-sharding case, invocationStarted is only called once, 181 // but for the sharding case, this might be called multiple times. 182 // Logic used to initialize the result directory should not be 183 // invoked twice during the same invocation. 184 initializeResultDirectories(); 185 } 186 } 187 } 188 189 /** 190 * Create directory structure where results and logs will be written. 191 */ initializeResultDirectories()192 private void initializeResultDirectories() { 193 info("Initializing result directory"); 194 195 try { 196 // Initialize the result directory. Either a new directory or reusing 197 // an existing session. 198 if (mRetrySessionId != null) { 199 // Overwrite the mResult with the test results of the previous session 200 mResult = ResultHandler.findResult(mBuildHelper.getResultsDir(), mRetrySessionId); 201 } 202 mResult.setStartTime(mBuildHelper.getStartTime()); 203 mResultDir = mBuildHelper.getResultDir(); 204 if (mResultDir != null) { 205 mResultDir.mkdirs(); 206 } 207 } catch (FileNotFoundException e) { 208 throw new RuntimeException(e); 209 } 210 211 if (mResultDir == null) { 212 throw new RuntimeException("Result Directory was not created"); 213 } 214 if (!mResultDir.exists()) { 215 throw new RuntimeException("Result Directory was not created: " + 216 mResultDir.getAbsolutePath()); 217 } 218 219 info("Results Directory: " + mResultDir.getAbsolutePath()); 220 221 mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName()); 222 try { 223 mLogDir = new File(mBuildHelper.getLogsDir(), 224 CompatibilityBuildHelper.getDirSuffix(mBuildHelper.getStartTime())); 225 } catch (FileNotFoundException e) { 226 e.printStackTrace(); 227 } 228 if (mLogDir != null && mLogDir.mkdirs()) { 229 info("Created log dir %s", mLogDir.getAbsolutePath()); 230 } 231 if (mLogDir == null || !mLogDir.exists()) { 232 throw new IllegalArgumentException(String.format("Could not create log dir %s", 233 mLogDir.getAbsolutePath())); 234 } 235 } 236 237 /** 238 * {@inheritDoc} 239 */ 240 @Override testRunStarted(String id, int numTests)241 public void testRunStarted(String id, int numTests) { 242 if (mCurrentModuleResult != null && mCurrentModuleResult.getId().equals(id)) { 243 // In case we get another test run of a known module, update the complete 244 // status to false to indicate it is not complete. This happens in cases like host side 245 // tests when each test class is executed as separate module. 246 mCurrentModuleResult.setDone(false); 247 mTotalTestsInModule += numTests; 248 } else { 249 mCurrentModuleResult = mResult.getOrCreateModule(id); 250 mTotalTestsInModule = numTests; 251 // Reset counters 252 mCurrentTestNum = 0; 253 } 254 } 255 256 /** 257 * {@inheritDoc} 258 */ 259 @Override testStarted(TestIdentifier test)260 public void testStarted(TestIdentifier test) { 261 mCurrentCaseResult = mCurrentModuleResult.getOrCreateResult(test.getClassName()); 262 mCurrentResult = mCurrentCaseResult.getOrCreateResult(test.getTestName().trim()); 263 mCurrentResult.reset(); 264 mCurrentTestNum++; 265 } 266 267 /** 268 * {@inheritDoc} 269 */ 270 @Override testEnded(TestIdentifier test, Map<String, String> metrics)271 public void testEnded(TestIdentifier test, Map<String, String> metrics) { 272 if (mCurrentResult.getResultStatus() == TestStatus.FAIL) { 273 // Test has previously failed. 274 return; 275 } 276 // device test can have performance results in test metrics 277 String perfResult = metrics.get(RESULT_KEY); 278 ReportLog report = null; 279 if (perfResult != null) { 280 try { 281 report = ReportLog.parse(perfResult); 282 } catch (XmlPullParserException | IOException e) { 283 e.printStackTrace(); 284 } 285 } else { 286 // host test should be checked into MetricsStore. 287 report = MetricsStore.removeResult(mBuildHelper.getBuildInfo(), 288 mCurrentModuleResult.getAbi(), test.toString()); 289 } 290 if (mCurrentResult.getResultStatus() == null) { 291 // Only claim that we passed when we're certain our result was 292 // not any other state. 293 mCurrentResult.passed(report); 294 } 295 } 296 297 /** 298 * {@inheritDoc} 299 */ 300 @Override testIgnored(TestIdentifier test)301 public void testIgnored(TestIdentifier test) { 302 // Ignored tests are not reported. 303 mCurrentTestNum--; 304 } 305 306 /** 307 * {@inheritDoc} 308 */ 309 @Override testFailed(TestIdentifier test, String trace)310 public void testFailed(TestIdentifier test, String trace) { 311 mCurrentResult.failed(trace); 312 } 313 314 /** 315 * {@inheritDoc} 316 */ 317 @Override testAssumptionFailure(TestIdentifier test, String trace)318 public void testAssumptionFailure(TestIdentifier test, String trace) { 319 mCurrentResult.skipped(); 320 } 321 322 /** 323 * {@inheritDoc} 324 */ 325 @Override testRunStopped(long elapsedTime)326 public void testRunStopped(long elapsedTime) { 327 // ignore 328 } 329 330 /** 331 * {@inheritDoc} 332 */ 333 @Override testRunEnded(long elapsedTime, Map<String, String> metrics)334 public void testRunEnded(long elapsedTime, Map<String, String> metrics) { 335 mCurrentModuleResult.addRuntime(elapsedTime); 336 // Expect them to be equal, but greater than to be safe. 337 mCurrentModuleResult.setDone(mCurrentTestNum >= mTotalTestsInModule); 338 339 if (isShardResultReporter()) { 340 // Forward module results to the master. 341 mMasterResultReporter.mergeModuleResult(mCurrentModuleResult); 342 } 343 } 344 345 /** 346 * Directly add a module result. Note: this method is meant to be used by 347 * a shard ResultReporter. 348 */ mergeModuleResult(IModuleResult moduleResult)349 private void mergeModuleResult(IModuleResult moduleResult) { 350 // This merges the results in moduleResult to any existing results already 351 // contained in mResult. This is useful for retries and allows the final 352 // report from a retry to contain all test results. 353 synchronized(this) { 354 mResult.mergeModuleResult(moduleResult); 355 } 356 } 357 358 /** 359 * {@inheritDoc} 360 */ 361 @Override testRunFailed(String errorMessage)362 public void testRunFailed(String errorMessage) { 363 // ignore 364 } 365 366 /** 367 * {@inheritDoc} 368 */ 369 @Override getSummary()370 public TestSummary getSummary() { 371 // ignore 372 return null; 373 } 374 375 /** 376 * {@inheritDoc} 377 */ 378 @Override putSummary(List<TestSummary> summaries)379 public void putSummary(List<TestSummary> summaries) { 380 // This is safe to be invoked on either the master or a shard ResultReporter, 381 // but the value added to the report will be that of the master ResultReporter. 382 if (summaries.size() > 0) { 383 mReferenceUrl = summaries.get(0).getSummary().getString(); 384 } 385 } 386 387 /** 388 * {@inheritDoc} 389 */ 390 @Override invocationEnded(long elapsedTime)391 public void invocationEnded(long elapsedTime) { 392 if (isShardResultReporter()) { 393 // Shard ResultReporters report 394 mMasterResultReporter.invocationEnded(elapsedTime); 395 return; 396 } 397 398 // NOTE: Everything after this line only applies to the master ResultReporter. 399 400 401 synchronized(this) { 402 // The master ResultReporter tracks the progress of all invocations across 403 // shard ResultReporters. Writing results should not proceed until all 404 // ResultReporters have completed. 405 if (++invocationEndedCount < mMasterBuildInfos.size()) { 406 return; 407 } 408 finalizeResults(elapsedTime); 409 } 410 } 411 finalizeResults(long elapsedTime)412 private void finalizeResults(long elapsedTime) { 413 // Add all device serials into the result to be serialized 414 for (String deviceSerial : mMasterDeviceSerials) { 415 mResult.addDeviceSerial(deviceSerial); 416 } 417 418 Set<String> allExpectedModules = new HashSet<>(); 419 // Add all build info to the result to be serialized 420 for (IBuildInfo buildInfo : mMasterBuildInfos) { 421 for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) { 422 String key = entry.getKey(); 423 String value = entry.getValue(); 424 if (key.startsWith(BUILD_INFO)) { 425 mResult.addInvocationInfo(key.substring(CTS_PREFIX.length()), value); 426 } 427 428 if (key.equals(CompatibilityBuildHelper.MODULE_IDS) && value.length() > 0) { 429 Collections.addAll(allExpectedModules, value.split(",")); 430 } 431 } 432 } 433 434 // Include a record in the report of all expected modules ids, even if they weren't 435 // executed. 436 for (String moduleId : allExpectedModules) { 437 mResult.getOrCreateModule(moduleId); 438 } 439 440 String moduleProgress = String.format("%d of %d", 441 mResult.getModuleCompleteCount(), mResult.getModules().size()); 442 443 info("Invocation finished in %s. PASSED: %d, FAILED: %d, MODULES: %s", 444 TimeUtil.formatElapsedTime(elapsedTime), 445 mResult.countResults(TestStatus.PASS), 446 mResult.countResults(TestStatus.FAIL), 447 moduleProgress); 448 449 long startTime = mResult.getStartTime(); 450 try { 451 File resultFile = ResultHandler.writeResults(mBuildHelper.getSuiteName(), 452 mBuildHelper.getSuiteVersion(), mBuildHelper.getSuitePlan(), 453 mBuildHelper.getSuiteBuild(), mResult, mResultDir, startTime, 454 elapsedTime + startTime, mReferenceUrl, getLogUrl(), 455 mBuildHelper.getCommandLineArgs()); 456 info("Test Result: %s", resultFile.getCanonicalPath()); 457 458 // Zip the full test results directory. 459 copyDynamicConfigFiles(mBuildHelper.getDynamicConfigFiles(), mResultDir); 460 copyFormattingFiles(mResultDir); 461 File zippedResults = zipResults(mResultDir); 462 info("Full Result: %s", zippedResults.getCanonicalPath()); 463 464 saveLog(resultFile, zippedResults); 465 466 uploadResult(resultFile); 467 468 } catch (IOException | XmlPullParserException e) { 469 CLog.e("[%s] Exception while saving result XML.", mDeviceSerial); 470 CLog.e(e); 471 } 472 } 473 474 /** 475 * {@inheritDoc} 476 */ 477 @Override invocationFailed(Throwable cause)478 public void invocationFailed(Throwable cause) { 479 warn("Invocation failed: %s", cause); 480 } 481 482 /** 483 * {@inheritDoc} 484 */ 485 @Override testLog(String name, LogDataType type, InputStreamSource stream)486 public void testLog(String name, LogDataType type, InputStreamSource stream) { 487 // This is safe to be invoked on either the master or a shard ResultReporter 488 if (isShardResultReporter()) { 489 // Shard ResultReporters forward testLog to the mMasterResultReporter 490 mMasterResultReporter.testLog(name, type, stream); 491 return; 492 } 493 try { 494 LogFileSaver saver = new LogFileSaver(mLogDir); 495 File logFile = saver.saveAndZipLogData(name, type, stream.createInputStream()); 496 info("Saved logs for %s in %s", name, logFile.getAbsolutePath()); 497 } catch (IOException e) { 498 warn("Failed to write log for %s", name); 499 e.printStackTrace(); 500 } 501 } 502 503 /** 504 * {@inheritDoc} 505 */ 506 @Override testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)507 public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, 508 LogFile logFile) { 509 // This is safe to be invoked on either the master or a shard ResultReporter 510 if (mIncludeTestLogTags && mCurrentResult != null 511 && dataName.startsWith(mCurrentResult.getFullName())) { 512 513 if (dataType == LogDataType.BUGREPORT) { 514 mCurrentResult.setBugReport(logFile.getUrl()); 515 } else if (dataType == LogDataType.LOGCAT) { 516 mCurrentResult.setLog(logFile.getUrl()); 517 } else if (dataType == LogDataType.PNG) { 518 mCurrentResult.setScreenshot(logFile.getUrl()); 519 } 520 } 521 } 522 523 /** 524 * {@inheritDoc} 525 */ 526 @Override setLogSaver(ILogSaver saver)527 public void setLogSaver(ILogSaver saver) { 528 // This is safe to be invoked on either the master or a shard ResultReporter 529 mLogSaver = saver; 530 } 531 532 /** 533 * When enabled, save log data using log saver 534 */ saveLog(File resultFile, File zippedResults)535 private void saveLog(File resultFile, File zippedResults) throws IOException { 536 if (!mUseLogSaver) { 537 return; 538 } 539 540 FileInputStream fis = null; 541 try { 542 fis = new FileInputStream(resultFile); 543 mLogSaver.saveLogData("log-result", LogDataType.XML, fis); 544 } catch (IOException ioe) { 545 CLog.e("[%s] error saving XML with log saver", mDeviceSerial); 546 CLog.e(ioe); 547 } finally { 548 StreamUtil.close(fis); 549 } 550 // Save the full results folder. 551 if (zippedResults != null) { 552 FileInputStream zipResultStream = null; 553 try { 554 zipResultStream = new FileInputStream(zippedResults); 555 mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream); 556 } finally { 557 StreamUtil.close(zipResultStream); 558 } 559 } 560 } 561 562 /** 563 * Return the path in which log saver persists log files or null if 564 * logSaver is not enabled. 565 */ getLogUrl()566 private String getLogUrl() { 567 if (!mUseLogSaver || mLogSaver == null) { 568 return null; 569 } 570 571 return mLogSaver.getLogReportDir().getUrl(); 572 } 573 574 @Override clone()575 public IShardableListener clone() { 576 ResultReporter clone = new ResultReporter(this); 577 OptionCopier.copyOptionsNoThrow(this, clone); 578 return clone; 579 } 580 581 /** 582 * Return true if this instance is a shard ResultReporter and should propagate 583 * certain events to the master. 584 */ isShardResultReporter()585 private boolean isShardResultReporter() { 586 return mMasterResultReporter != null; 587 } 588 589 /** 590 * When enabled, upload the result to a server. 591 */ uploadResult(File resultFile)592 private void uploadResult(File resultFile) throws IOException { 593 if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) { 594 try { 595 info("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl)); 596 } catch (IOException ioe) { 597 CLog.e("[%s] IOException while uploading result.", mDeviceSerial); 598 CLog.e(ioe); 599 } 600 } 601 } 602 603 /** 604 * Copy the xml formatting files stored in this jar to the results directory 605 * 606 * @param resultsDir 607 */ copyFormattingFiles(File resultsDir)608 static void copyFormattingFiles(File resultsDir) { 609 for (String resultFileName : RESULT_RESOURCES) { 610 InputStream configStream = ResultHandler.class.getResourceAsStream( 611 String.format("/report/%s", resultFileName)); 612 if (configStream != null) { 613 File resultFile = new File(resultsDir, resultFileName); 614 try { 615 FileUtil.writeToFile(configStream, resultFile); 616 } catch (IOException e) { 617 warn("Failed to write %s to file", resultFileName); 618 } 619 } else { 620 warn("Failed to load %s from jar", resultFileName); 621 } 622 } 623 } 624 625 /** 626 * move the dynamic config files to the results directory 627 * 628 * @param configFiles 629 * @param resultsDir 630 */ copyDynamicConfigFiles(Map<String, File> configFiles, File resultsDir)631 static void copyDynamicConfigFiles(Map<String, File> configFiles, File resultsDir) { 632 if (configFiles.size() == 0) return; 633 634 File folder = new File(resultsDir, "config"); 635 folder.mkdir(); 636 for (String moduleName : configFiles.keySet()) { 637 File resultFile = new File(folder, moduleName+".dynamic"); 638 try { 639 FileUtil.copyFile(configFiles.get(moduleName), resultFile); 640 FileUtil.deleteFile(configFiles.get(moduleName)); 641 } catch (IOException e) { 642 warn("Failed to copy config file for %s to file", moduleName); 643 } 644 } 645 } 646 647 /** 648 * Zip the contents of the given results directory. 649 * 650 * @param resultsDir 651 */ zipResults(File resultsDir)652 private static File zipResults(File resultsDir) { 653 File zipResultFile = null; 654 try { 655 // create a file in parent directory, with same name as resultsDir 656 zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip", 657 resultsDir.getName())); 658 ZipUtil.createZip(resultsDir, zipResultFile); 659 } catch (IOException e) { 660 warn("Failed to create zip for %s", resultsDir.getName()); 661 } 662 return zipResultFile; 663 } 664 665 /** 666 * Log info to the console. 667 */ info(String format, Object... args)668 private static void info(String format, Object... args) { 669 log(LogLevel.INFO, format, args); 670 } 671 672 /** 673 * Log a warning to the console. 674 */ warn(String format, Object... args)675 private static void warn(String format, Object... args) { 676 log(LogLevel.WARN, format, args); 677 } 678 679 /** 680 * Log a message to the console 681 */ log(LogLevel level, String format, Object... args)682 private static void log(LogLevel level, String format, Object... args) { 683 CLog.logAndDisplay(level, format, args); 684 } 685 686 /** 687 * For testing 688 */ getResult()689 IInvocationResult getResult() { 690 return mResult; 691 } 692 } 693