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