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.retry.RetryFactoryTest;
20 import com.android.compatibility.common.tradefed.testtype.suite.CompatibilityTestSuite;
21 import com.android.compatibility.common.tradefed.util.FingerprintComparisonException;
22 import com.android.compatibility.common.tradefed.util.RetryType;
23 import com.android.compatibility.common.util.ChecksumReporter;
24 import com.android.compatibility.common.util.DeviceInfo;
25 import com.android.compatibility.common.util.ICaseResult;
26 import com.android.compatibility.common.util.IInvocationResult;
27 import com.android.compatibility.common.util.IModuleResult;
28 import com.android.compatibility.common.util.ITestResult;
29 import com.android.compatibility.common.util.InvocationResult;
30 import com.android.compatibility.common.util.InvocationResult.RunHistory;
31 import com.android.compatibility.common.util.MetricsStore;
32 import com.android.compatibility.common.util.ReportLog;
33 import com.android.compatibility.common.util.ResultHandler;
34 import com.android.compatibility.common.util.ResultUploader;
35 import com.android.compatibility.common.util.TestStatus;
36 import com.android.ddmlib.Log.LogLevel;
37 import com.android.tradefed.build.IBuildInfo;
38 import com.android.tradefed.config.IConfiguration;
39 import com.android.tradefed.config.IConfigurationReceiver;
40 import com.android.tradefed.config.Option;
41 import com.android.tradefed.config.Option.Importance;
42 import com.android.tradefed.config.OptionClass;
43 import com.android.tradefed.config.OptionCopier;
44 import com.android.tradefed.invoker.IInvocationContext;
45 import com.android.tradefed.log.LogUtil.CLog;
46 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
47 import com.android.tradefed.result.FileInputStreamSource;
48 import com.android.tradefed.result.ILogSaver;
49 import com.android.tradefed.result.ILogSaverListener;
50 import com.android.tradefed.result.IShardableListener;
51 import com.android.tradefed.result.ITestInvocationListener;
52 import com.android.tradefed.result.ITestSummaryListener;
53 import com.android.tradefed.result.InputStreamSource;
54 import com.android.tradefed.result.LogDataType;
55 import com.android.tradefed.result.LogFile;
56 import com.android.tradefed.result.LogFileSaver;
57 import com.android.tradefed.result.TestDescription;
58 import com.android.tradefed.result.TestSummary;
59 import com.android.tradefed.result.suite.SuiteResultReporter;
60 import com.android.tradefed.util.FileUtil;
61 import com.android.tradefed.util.StreamUtil;
62 import com.android.tradefed.util.TimeUtil;
63 import com.android.tradefed.util.ZipUtil;
64 import com.android.tradefed.util.proto.TfMetricProtoUtil;
65 
66 import com.google.common.annotations.VisibleForTesting;
67 import com.google.common.xml.XmlEscapers;
68 import com.google.gson.Gson;
69 
70 import org.xmlpull.v1.XmlPullParserException;
71 
72 import java.io.File;
73 import java.io.FileInputStream;
74 import java.io.FileNotFoundException;
75 import java.io.IOException;
76 import java.io.InputStream;
77 import java.nio.file.Files;
78 import java.nio.file.Path;
79 import java.util.Arrays;
80 import java.util.Collection;
81 import java.util.Collections;
82 import java.util.HashMap;
83 import java.util.HashSet;
84 import java.util.List;
85 import java.util.Map;
86 import java.util.Set;
87 import java.util.concurrent.CountDownLatch;
88 import java.util.concurrent.TimeUnit;
89 
90 /**
91  * Collect test results for an entire invocation and output test results to disk.
92  */
93 @OptionClass(alias="result-reporter")
94 public class ResultReporter implements ILogSaverListener, ITestInvocationListener,
95        ITestSummaryListener, IShardableListener, IConfigurationReceiver {
96 
97     public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip";
98     private static final String UNKNOWN_DEVICE = "unknown_device";
99     private static final String RESULT_KEY = "COMPATIBILITY_TEST_RESULT";
100     private static final String CTS_PREFIX = "cts:";
101     private static final String BUILD_INFO = CTS_PREFIX + "build_";
102     private static final String LATEST_LINK_NAME = "latest";
103     /** Used to get run history from the test result of last run. */
104     private static final String RUN_HISTORY_KEY = "run_history";
105 
106 
107     public static final String BUILD_BRAND = "build_brand";
108     public static final String BUILD_DEVICE = "build_device";
109     public static final String BUILD_FINGERPRINT = "build_fingerprint";
110     public static final String BUILD_ID = "build_id";
111     public static final String BUILD_MANUFACTURER = "build_manufacturer";
112     public static final String BUILD_MODEL = "build_model";
113     public static final String BUILD_PRODUCT = "build_product";
114     public static final String BUILD_VERSION_RELEASE = "build_version_release";
115 
116     private static final List<String> NOT_RETRY_FILES = Arrays.asList(
117             ChecksumReporter.NAME,
118             ChecksumReporter.PREV_NAME,
119             ResultHandler.FAILURE_REPORT_NAME,
120             "diffs");
121 
122     @Option(name = RetryFactoryTest.RETRY_OPTION,
123             shortName = 'r',
124             description = "retry a previous session.",
125             importance = Importance.IF_UNSET)
126     private Integer mRetrySessionId = null;
127 
128     @Option(name = RetryFactoryTest.RETRY_TYPE_OPTION,
129             description = "used with " + RetryFactoryTest.RETRY_OPTION
130             + ", retry tests of a certain status. Possible values include \"failed\", "
131             + "\"not_executed\", and \"custom\".",
132             importance = Importance.IF_UNSET)
133     private RetryType mRetryType = null;
134 
135     @Option(name = "result-server", description = "Server to publish test results.")
136     private String mResultServer;
137 
138     @Option(name = "disable-result-posting", description = "Disable result posting into report server.")
139     private boolean mDisableResultPosting = false;
140 
141     @Option(name = "include-test-log-tags", description = "Include test log tags in report.")
142     private boolean mIncludeTestLogTags = false;
143 
144     @Option(name = "use-log-saver", description = "Also saves generated result with log saver")
145     private boolean mUseLogSaver = false;
146 
147     @Option(name = "compress-logs", description = "Whether logs will be saved with compression")
148     private boolean mCompressLogs = true;
149 
150     @Option(name = INCLUDE_HTML_IN_ZIP,
151             description = "Whether failure summary report is included in the zip fie.")
152     private boolean mIncludeHtml = false;
153 
154     @Option(
155             name = "result-attribute",
156             description =
157                     "Extra key-value pairs to be added as attributes and corresponding"
158                             + "values of the \"Result\" tag in the result XML.")
159     private Map<String, String> mResultAttributes = new HashMap<String, String>();
160 
161     private CompatibilityBuildHelper mBuildHelper;
162     private File mResultDir = null;
163     private File mLogDir = null;
164     private ResultUploader mUploader;
165     private String mReferenceUrl;
166     private ILogSaver mLogSaver;
167     private int invocationEndedCount = 0;
168     private CountDownLatch mFinalized = null;
169 
170     protected IInvocationResult mResult = new InvocationResult();
171     private IModuleResult mCurrentModuleResult;
172     private ICaseResult mCurrentCaseResult;
173     private ITestResult mCurrentResult;
174     private String mDeviceSerial = UNKNOWN_DEVICE;
175     private Set<String> mMainDeviceSerials = new HashSet<>();
176     private Set<IBuildInfo> mMainBuildInfos = new HashSet<>();
177     // Whether or not we failed the fingerprint check
178     private boolean mFingerprintFailure = false;
179 
180     // mCurrentTestNum and mTotalTestsInModule track the progress within the module
181     // Note that this count is not necessarily equal to the count of tests contained
182     // in mCurrentModuleResult because of how special cases like ignored tests are reported.
183     private int mCurrentTestNum;
184     private int mTotalTestsInModule;
185 
186     // Whether modules can be marked done for this invocation. Initialized in invocationStarted()
187     // Visible for unit testing
188     protected boolean mCanMarkDone;
189     // Whether the current test run has failed. If true, we will not mark the current module done
190     protected boolean mTestRunFailed;
191     // Whether the current module has previously been marked done
192     private boolean mModuleWasDone;
193 
194     // Nullable. If null, "this" is considered the primary and must handle
195     // result aggregation and reporting. When not null, it should forward events to the primary
196     private final ResultReporter mPrimaryResultReporter;
197 
198     private LogFileSaver mTestLogSaver;
199 
200     // Elapsed time from invocation started to ended.
201     private long mElapsedTime;
202 
203     /** Invocation level configuration */
204     private IConfiguration mConfiguration = null;
205 
206     /**
207      * Default constructor.
208      */
ResultReporter()209     public ResultReporter() {
210         this(null);
211         mFinalized = new CountDownLatch(1);
212     }
213 
214     /**
215      * Construct a shard ResultReporter that forwards module results to the mPrimaryResultReporter.
216      */
ResultReporter(ResultReporter primaryResultReporter)217     public ResultReporter(ResultReporter primaryResultReporter) {
218         mPrimaryResultReporter = primaryResultReporter;
219     }
220 
221     /** {@inheritDoc} */
222     @Override
setConfiguration(IConfiguration configuration)223     public void setConfiguration(IConfiguration configuration) {
224         mConfiguration = configuration;
225     }
226 
227     /**
228      * {@inheritDoc}
229      */
230     @Override
invocationStarted(IInvocationContext context)231     public void invocationStarted(IInvocationContext context) {
232         IBuildInfo primaryBuild = context.getBuildInfos().get(0);
233         synchronized(this) {
234             if (mBuildHelper == null) {
235                 mBuildHelper = new CompatibilityBuildHelper(primaryBuild);
236             }
237             if (mDeviceSerial == null && primaryBuild.getDeviceSerial() != null) {
238                 mDeviceSerial = primaryBuild.getDeviceSerial();
239             }
240             mCanMarkDone = canMarkDone(mBuildHelper.getRecentCommandLineArgs());
241         }
242 
243         if (isShardResultReporter()) {
244             // Shard ResultReporters forward invocationStarted to the mPrimaryResultReporter
245             mPrimaryResultReporter.invocationStarted(context);
246             return;
247         }
248 
249         // NOTE: Everything after this line only applies to the primary ResultReporter.
250 
251         synchronized (this) {
252             if (primaryBuild.getDeviceSerial() != null) {
253                 // The primary ResultReporter collects all device serials being used
254                 // for the current implementation.
255                 mMainDeviceSerials.add(primaryBuild.getDeviceSerial());
256             }
257 
258             // The primary ResultReporter collects all buildInfos.
259             mMainBuildInfos.add(primaryBuild);
260 
261             if (mResultDir == null) {
262                 // For the non-sharding case, invocationStarted is only called once,
263                 // but for the sharding case, this might be called multiple times.
264                 // Logic used to initialize the result directory should not be
265                 // invoked twice during the same invocation.
266                 initializeResultDirectories();
267             }
268         }
269     }
270 
271     /**
272      * Create directory structure where results and logs will be written.
273      */
initializeResultDirectories()274     private void initializeResultDirectories() {
275         debug("Initializing result directory");
276 
277         try {
278             // Initialize the result directory. Either a new directory or reusing
279             // an existing session.
280             if (mRetrySessionId != null) {
281                 // Overwrite the mResult with the test results of the previous session
282                 mResult = ResultHandler.findResult(mBuildHelper.getResultsDir(), mRetrySessionId);
283             }
284             mResult.setStartTime(mBuildHelper.getStartTime());
285             mResultDir = mBuildHelper.getResultDir();
286             if (mResultDir != null) {
287                 mResultDir.mkdirs();
288             }
289         } catch (FileNotFoundException e) {
290             throw new RuntimeException(e);
291         }
292 
293         if (mResultDir == null) {
294             throw new RuntimeException("Result Directory was not created");
295         }
296         if (!mResultDir.exists()) {
297             throw new RuntimeException("Result Directory was not created: " +
298                     mResultDir.getAbsolutePath());
299         }
300 
301         debug("Results Directory: %s", mResultDir.getAbsolutePath());
302 
303         mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName());
304         try {
305             mLogDir = new File(mBuildHelper.getLogsDir(),
306                     CompatibilityBuildHelper.getDirSuffix(mBuildHelper.getStartTime()));
307         } catch (FileNotFoundException e) {
308             CLog.e(e);
309         }
310         if (mLogDir != null && mLogDir.mkdirs()) {
311             debug("Created log dir %s", mLogDir.getAbsolutePath());
312         }
313         if (mLogDir == null || !mLogDir.exists()) {
314             throw new IllegalArgumentException(String.format("Could not create log dir %s",
315                     mLogDir.getAbsolutePath()));
316         }
317         if (mTestLogSaver == null) {
318             mTestLogSaver = new LogFileSaver(mLogDir);
319         }
320     }
321 
322     /**
323      * {@inheritDoc}
324      */
325     @Override
testRunStarted(String id, int numTests)326     public void testRunStarted(String id, int numTests) {
327         if (mCurrentModuleResult != null && mCurrentModuleResult.getId().equals(id)
328                 && mCurrentModuleResult.isDone()) {
329             // Modules run with JarHostTest treat each test class as a separate module,
330             // resulting in additional unexpected test runs.
331             // This case exists only for N
332             mTotalTestsInModule += numTests;
333         } else {
334             // Handle non-JarHostTest case
335             mCurrentModuleResult = mResult.getOrCreateModule(id);
336             mModuleWasDone = mCurrentModuleResult.isDone();
337             mTestRunFailed = false;
338             if (!mModuleWasDone) {
339                 // we only want to update testRun variables if the IModuleResult is not yet done
340                 // otherwise leave testRun variables alone so isDone evaluates to true.
341                 if (mCurrentModuleResult.getExpectedTestRuns() == 0) {
342                     mCurrentModuleResult.setExpectedTestRuns(TestRunHandler.getTestRuns(
343                             mBuildHelper, mCurrentModuleResult.getId()));
344                 }
345                 mCurrentModuleResult.addTestRun();
346             }
347             // Reset counters
348             mTotalTestsInModule = numTests;
349             mCurrentTestNum = 0;
350         }
351         mCurrentModuleResult.inProgress(true);
352     }
353 
354     /**
355      * {@inheritDoc}
356      */
357     @Override
testStarted(TestDescription test)358     public void testStarted(TestDescription test) {
359         mCurrentCaseResult = mCurrentModuleResult.getOrCreateResult(test.getClassName());
360         mCurrentResult = mCurrentCaseResult.getOrCreateResult(test.getTestName().trim());
361         if (mCurrentResult.isRetry()) {
362             mCurrentResult.reset(); // clear result status for this invocation
363         }
364         mCurrentTestNum++;
365     }
366 
367     /**
368      * {@inheritDoc}
369      */
370     @Override
testEnded(TestDescription test, HashMap<String, Metric> metrics)371     public void testEnded(TestDescription test, HashMap<String, Metric> metrics) {
372         if (mCurrentResult.getResultStatus() == TestStatus.FAIL) {
373             // Test has previously failed.
374             return;
375         }
376         // device test can have performance results in test metrics
377         Metric perfResult = metrics.get(RESULT_KEY);
378         ReportLog report = null;
379         if (perfResult != null) {
380             try {
381                 report = ReportLog.parse(perfResult.getMeasurements().getSingleString());
382             } catch (XmlPullParserException | IOException e) {
383                 e.printStackTrace();
384             }
385         } else {
386             // host test should be checked into MetricsStore.
387             report = MetricsStore.removeResult(mBuildHelper.getBuildInfo(),
388                     mCurrentModuleResult.getAbi(), test.toString());
389         }
390         if (mCurrentResult.getResultStatus() == null) {
391             // Only claim that we passed when we're certain our result was
392             // not any other state.
393             mCurrentResult.passed(report);
394         }
395     }
396 
397     /**
398      * {@inheritDoc}
399      */
400     @Override
testIgnored(TestDescription test)401     public void testIgnored(TestDescription test) {
402         mCurrentResult.skipped();
403     }
404 
405     /**
406      * {@inheritDoc}
407      */
408     @Override
testFailed(TestDescription test, String trace)409     public void testFailed(TestDescription test, String trace) {
410         mCurrentResult.failed(sanitizeXmlContent(trace));
411     }
412 
413     /**
414      * {@inheritDoc}
415      */
416     @Override
testAssumptionFailure(TestDescription test, String trace)417     public void testAssumptionFailure(TestDescription test, String trace) {
418         mCurrentResult.skipped();
419     }
420 
421     /**
422      * {@inheritDoc}
423      */
424     @Override
testRunStopped(long elapsedTime)425     public void testRunStopped(long elapsedTime) {
426         // ignore
427     }
428 
429     /**
430      * {@inheritDoc}
431      */
432     @Override
testRunEnded(long elapsedTime, Map<String, String> metrics)433     public void testRunEnded(long elapsedTime, Map<String, String> metrics) {
434         testRunEnded(elapsedTime, TfMetricProtoUtil.upgradeConvert(metrics));
435     }
436 
437     /**
438      * {@inheritDoc}
439      */
440     @Override
testRunEnded(long elapsedTime, HashMap<String, Metric> metrics)441     public void testRunEnded(long elapsedTime, HashMap<String, Metric> metrics) {
442         mCurrentModuleResult.inProgress(false);
443         mCurrentModuleResult.addRuntime(elapsedTime);
444         if (!mModuleWasDone && mCanMarkDone) {
445             if (mTestRunFailed) {
446                 // set done to false for test run failures
447                 mCurrentModuleResult.setDone(false);
448             } else {
449                 // Only mark module done if:
450                 // - status of the invocation allows it (mCanMarkDone), and
451                 // - module has not already been marked done, and
452                 // - no test run failure has been detected
453                 mCurrentModuleResult.setDone(mCurrentTestNum >= mTotalTestsInModule);
454             }
455         }
456         if (isShardResultReporter()) {
457             // Forward module results to the primary.
458             mPrimaryResultReporter.mergeModuleResult(mCurrentModuleResult);
459             mCurrentModuleResult.resetTestRuns();
460             mCurrentModuleResult.resetRuntime();
461         }
462     }
463 
464     /**
465      * Directly add a module result. Note: this method is meant to be used by
466      * a shard ResultReporter.
467      */
mergeModuleResult(IModuleResult moduleResult)468     private void mergeModuleResult(IModuleResult moduleResult) {
469         // This merges the results in moduleResult to any existing results already
470         // contained in mResult. This is useful for retries and allows the final
471         // report from a retry to contain all test results.
472         synchronized(this) {
473             mResult.mergeModuleResult(moduleResult);
474         }
475     }
476 
477     /**
478      * {@inheritDoc}
479      */
480     @Override
testRunFailed(String errorMessage)481     public void testRunFailed(String errorMessage) {
482         mTestRunFailed = true;
483         mCurrentModuleResult.setFailed();
484     }
485 
486     /**
487      * {@inheritDoc}
488      */
489     @Override
getSummary()490     public TestSummary getSummary() {
491         // ignore
492         return null;
493     }
494 
495     /**
496      * {@inheritDoc}
497      */
498     @Override
putSummary(List<TestSummary> summaries)499     public void putSummary(List<TestSummary> summaries) {
500         for (TestSummary summary : summaries) {
501             // If one summary is from SuiteResultReporter, log it as an extra file.
502             if (SuiteResultReporter.SUITE_REPORTER_SOURCE.equals(summary.getSource())) {
503                 File summaryFile = null;
504                 try {
505                     summaryFile = FileUtil.createTempFile("summary", ".txt");
506                     FileUtil.writeToFile(summary.getSummary().getString(), summaryFile);
507                     try (InputStreamSource stream = new FileInputStreamSource(summaryFile)) {
508                         testLog("summary", LogDataType.TEXT, stream);
509                     }
510                 } catch (IOException e) {
511                     CLog.e(e);
512                 } finally {
513                     FileUtil.deleteFile(summaryFile);
514                 }
515             } else if (mReferenceUrl == null && summary.getSummary().getString() != null) {
516                 mReferenceUrl = summary.getSummary().getString();
517             }
518         }
519     }
520 
521     /**
522      * {@inheritDoc}
523      */
524     @Override
invocationEnded(long elapsedTime)525     public void invocationEnded(long elapsedTime) {
526         if (isShardResultReporter()) {
527             // Shard ResultReporters report
528             mPrimaryResultReporter.invocationEnded(elapsedTime);
529             return;
530         }
531 
532         // NOTE: Everything after this line only applies to the primary ResultReporter.
533 
534         synchronized (this) {
535             // The mPrimaryResultReporter tracks the progress of all invocations across
536             // shard ResultReporters. Writing results should not proceed until all
537             // ResultReporters have completed.
538             if (++invocationEndedCount < mMainBuildInfos.size()) {
539                 return;
540             }
541             mElapsedTime = elapsedTime;
542             finalizeResults();
543             mFinalized.countDown();
544         }
545     }
546 
547     /**
548      * Returns whether a report creation should be skipped.
549      */
shouldSkipReportCreation()550     protected boolean shouldSkipReportCreation() {
551         // This value is always false here for backwards compatibility.
552         // Extended classes have the option to override this.
553         return false;
554     }
555 
finalizeResults()556     private void finalizeResults() {
557         if (mFingerprintFailure) {
558             CLog.w("Failed the fingerprint check. Skip result reporting.");
559             return;
560         }
561         // Add all device serials into the result to be serialized
562         for (String deviceSerial : mMainDeviceSerials) {
563             mResult.addDeviceSerial(deviceSerial);
564         }
565 
566         addDeviceBuildInfoToResult();
567 
568         Set<String> allExpectedModules = new HashSet<>();
569         for (IBuildInfo buildInfo : mMainBuildInfos) {
570             for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) {
571                 String key = entry.getKey();
572                 String value = entry.getValue();
573                 if (key.equals(CompatibilityBuildHelper.MODULE_IDS) && value.length() > 0) {
574                     Collections.addAll(allExpectedModules, value.split(","));
575                 }
576             }
577         }
578 
579         // Include a record in the report of all expected modules ids, even if they weren't
580         // executed.
581         for (String moduleId : allExpectedModules) {
582             mResult.getOrCreateModule(moduleId);
583         }
584 
585         String moduleProgress = String.format("%d of %d",
586                 mResult.getModuleCompleteCount(), mResult.getModules().size());
587 
588 
589         if (shouldSkipReportCreation()) {
590             return;
591         }
592 
593         // Get run history from the test result of last run and add the run history of the current
594         // run to it.
595         // TODO(b/137973382): avoid casting by move the method to interface level.
596         Collection<RunHistory> runHistories = ((InvocationResult) mResult).getRunHistories();
597         String runHistoryJSON = mResult.getInvocationInfo().get(RUN_HISTORY_KEY);
598         Gson gson = new Gson();
599         if (runHistoryJSON != null) {
600             RunHistory[] runHistoryArray = gson.fromJson(runHistoryJSON, RunHistory[].class);
601             Collections.addAll(runHistories, runHistoryArray);
602         }
603         RunHistory newRun = new RunHistory();
604         newRun.startTime = mResult.getStartTime();
605         newRun.endTime = newRun.startTime + mElapsedTime;
606         runHistories.add(newRun);
607         mResult.addInvocationInfo(RUN_HISTORY_KEY, gson.toJson(runHistories));
608 
609         try {
610             // Zip the full test results directory.
611             copyDynamicConfigFiles();
612             copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName());
613 
614             File resultFile = generateResultXmlFile();
615             if (mRetrySessionId != null) {
616                 copyRetryFiles(ResultHandler.getResultDirectory(
617                         mBuildHelper.getResultsDir(), mRetrySessionId), mResultDir);
618             }
619             File failureReport = null;
620             if (mIncludeHtml) {
621                 // Create the html report before the zip file.
622                 failureReport = ResultHandler.createFailureReport(resultFile);
623             }
624             File zippedResults = zipResults(mResultDir);
625             if (!mIncludeHtml) {
626                 // Create failure report after zip file so extra data is not uploaded
627                 failureReport = ResultHandler.createFailureReport(resultFile);
628             }
629             if (failureReport != null && failureReport.exists()) {
630                 info("Test Result: %s", failureReport.getCanonicalPath());
631             } else {
632                 info("Test Result: %s", resultFile.getCanonicalPath());
633             }
634             info("Test Logs: %s", mLogDir.getCanonicalPath());
635             debug("Full Result: %s", zippedResults.getCanonicalPath());
636 
637             Path latestLink = createLatestLinkDirectory(mResultDir.toPath());
638             if (latestLink != null) {
639                 info("Latest results link: " + latestLink.toAbsolutePath());
640             }
641 
642             latestLink = createLatestLinkDirectory(mLogDir.toPath());
643             if (latestLink != null) {
644                 info("Latest logs link: " + latestLink.toAbsolutePath());
645             }
646 
647             saveLog(resultFile, zippedResults);
648 
649             uploadResult(resultFile);
650 
651         } catch (IOException | XmlPullParserException e) {
652             CLog.e("[%s] Exception while saving result XML.", mDeviceSerial);
653             CLog.e(e);
654         }
655         // print the run results last.
656         info("Invocation finished in %s. PASSED: %d, FAILED: %d, MODULES: %s",
657                 TimeUtil.formatElapsedTime(mElapsedTime),
658                 mResult.countResults(TestStatus.PASS),
659                 mResult.countResults(TestStatus.FAIL),
660                 moduleProgress);
661     }
662 
createLatestLinkDirectory(Path directory)663     private Path createLatestLinkDirectory(Path directory) {
664         Path link = null;
665 
666         Path parent = directory.getParent();
667 
668         if (parent != null) {
669             link = parent.resolve(LATEST_LINK_NAME);
670             try {
671                 // if latest already exists, we have to remove it before creating
672                 Files.deleteIfExists(link);
673                 Files.createSymbolicLink(link, directory);
674             } catch (IOException ioe) {
675                 CLog.e("Exception while attempting to create 'latest' link to: [%s]",
676                     directory);
677                 CLog.e(ioe);
678                 return null;
679             } catch (UnsupportedOperationException uoe) {
680                 CLog.e("Failed to create 'latest' symbolic link - unsupported operation");
681                 return null;
682             }
683         }
684         return link;
685     }
686 
687     /**
688      * {@inheritDoc}
689      */
690     @Override
invocationFailed(Throwable cause)691     public void invocationFailed(Throwable cause) {
692         warn("Invocation failed: %s", cause);
693         InvocationFailureHandler.setFailed(mBuildHelper, cause);
694         if (cause instanceof FingerprintComparisonException) {
695             mFingerprintFailure = true;
696         }
697     }
698 
699     /**
700      * {@inheritDoc}
701      */
702     @Override
testLog(String name, LogDataType type, InputStreamSource stream)703     public void testLog(String name, LogDataType type, InputStreamSource stream) {
704         // This is safe to be invoked on either the primary or a shard ResultReporter
705         if (isShardResultReporter()) {
706             // Shard ResultReporters forward testLog to the mPrimaryResultReporter
707             mPrimaryResultReporter.testLog(name, type, stream);
708             return;
709         }
710         if (name.endsWith(DeviceInfo.FILE_SUFFIX)) {
711             // Handle device info file case
712             testLogDeviceInfo(name, stream);
713         } else {
714             // Handle default case
715             try {
716                 File logFile = null;
717                 if (mCompressLogs) {
718                     try (InputStream inputStream = stream.createInputStream()) {
719                         logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream);
720                     }
721                 } else {
722                     try (InputStream inputStream = stream.createInputStream()) {
723                         logFile = mTestLogSaver.saveLogData(name, type, inputStream);
724                     }
725                 }
726                 debug("Saved logs for %s in %s", name, logFile.getAbsolutePath());
727             } catch (IOException e) {
728                 warn("Failed to write log for %s", name);
729                 CLog.e(e);
730             }
731         }
732     }
733 
734     /* Write device-info files to the result, invoked only by the primary result reporter */
testLogDeviceInfo(String name, InputStreamSource stream)735     private void testLogDeviceInfo(String name, InputStreamSource stream) {
736         try {
737             File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME);
738             ediDir.mkdirs();
739             File ediFile = new File(ediDir, name);
740             if (!ediFile.exists()) {
741                 // only write this file to the results if not already present
742                 FileUtil.writeToFile(stream.createInputStream(), ediFile);
743             }
744         } catch (IOException e) {
745             warn("Failed to write device info %s to result", name);
746             CLog.e(e);
747         }
748     }
749 
750     /**
751      * {@inheritDoc}
752      */
753     @Override
testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)754     public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
755             LogFile logFile) {
756         // This is safe to be invoked on either the primary or a shard ResultReporter
757         if (mIncludeTestLogTags
758                 && mCurrentResult != null
759                 && dataName.startsWith(mCurrentResult.getFullName())) {
760 
761             if (dataType == LogDataType.BUGREPORT) {
762                 mCurrentResult.setBugReport(logFile.getUrl());
763             } else if (dataType == LogDataType.LOGCAT) {
764                 mCurrentResult.setLog(logFile.getUrl());
765             } else if (dataType == LogDataType.PNG) {
766                 mCurrentResult.setScreenshot(logFile.getUrl());
767             }
768         }
769     }
770 
771     /**
772      * {@inheritDoc}
773      */
774     @Override
setLogSaver(ILogSaver saver)775     public void setLogSaver(ILogSaver saver) {
776         // This is safe to be invoked on either the primary or a shard ResultReporter
777         mLogSaver = saver;
778     }
779 
780     /**
781      * When enabled, save log data using log saver
782      */
saveLog(File resultFile, File zippedResults)783     private void saveLog(File resultFile, File zippedResults) throws IOException {
784         if (!mUseLogSaver) {
785             return;
786         }
787 
788         FileInputStream fis = null;
789         LogFile logFile = null;
790         try {
791             fis = new FileInputStream(resultFile);
792             logFile = mLogSaver.saveLogData("log-result", LogDataType.XML, fis);
793             debug("Result XML URL: %s", logFile.getUrl());
794             logReportFiles(mConfiguration, resultFile, resultFile.getName(), LogDataType.XML);
795         } catch (IOException ioe) {
796             CLog.e("[%s] error saving XML with log saver", mDeviceSerial);
797             CLog.e(ioe);
798         } finally {
799             StreamUtil.close(fis);
800         }
801         // Save the full results folder.
802         if (zippedResults != null) {
803             FileInputStream zipResultStream = null;
804             try {
805                 zipResultStream = new FileInputStream(zippedResults);
806                 logFile = mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream);
807                 debug("Result zip URL: %s", logFile.getUrl());
808                 logReportFiles(
809                         mConfiguration, zippedResults, "results", LogDataType.ZIP);
810             } finally {
811                 StreamUtil.close(zipResultStream);
812             }
813         }
814     }
815 
816     /**
817      * Return the path in which log saver persists log files or null if
818      * logSaver is not enabled.
819      */
getLogUrl()820     private String getLogUrl() {
821         if (!mUseLogSaver || mLogSaver == null) {
822             return null;
823         }
824 
825         return mLogSaver.getLogReportDir().getUrl();
826     }
827 
828     @Override
clone()829     public IShardableListener clone() {
830         ResultReporter clone = new ResultReporter(this);
831         OptionCopier.copyOptionsNoThrow(this, clone);
832         return clone;
833     }
834 
835     /**
836      * Create results file compatible with CTSv2 (xml) report format.
837      */
generateResultXmlFile()838     protected File generateResultXmlFile()
839             throws IOException, XmlPullParserException {
840         return ResultHandler.writeResults(
841                 mBuildHelper.getSuiteName(),
842                 mBuildHelper.getSuiteVersion(),
843                 getSuitePlan(mBuildHelper),
844                 mBuildHelper.getSuiteBuild(),
845                 mResult,
846                 mResultDir,
847                 mResult.getStartTime(),
848                 mElapsedTime + mResult.getStartTime(),
849                 mReferenceUrl,
850                 getLogUrl(),
851                 mBuildHelper.getCommandLineArgs(),
852                 mResultAttributes);
853     }
854 
855     /**
856      * Add build info collected from the device attributes to the results.
857      */
addDeviceBuildInfoToResult()858     protected void addDeviceBuildInfoToResult() {
859         // Add all build info to the result to be serialized
860         Map<String, String> buildProperties = mapBuildInfo();
861         addBuildInfoToResult(buildProperties, mResult);
862     }
863 
864     /**
865      * Override specific build properties so the report will be associated with the
866      * build fingerprint being certified.
867      */
addDeviceBuildInfoToResult(String buildFingerprintOverride, String manufactureOverride, String modelOverride)868     protected void addDeviceBuildInfoToResult(String buildFingerprintOverride,
869             String manufactureOverride, String modelOverride) {
870 
871         Map<String, String> buildProperties = mapBuildInfo();
872 
873         // Extract and override values from build fingerprint.
874         // Build fingerprint format: brand/product/device:version/build_id/tags
875         String fingerprintPrefix = buildFingerprintOverride.split(":")[0];
876         String fingerprintTail = buildFingerprintOverride.split(":")[1];
877         String buildIdOverride = fingerprintTail.split("/")[1];
878         buildProperties.put(BUILD_ID, buildIdOverride);
879         String brandOverride = fingerprintPrefix.split("/")[0];
880         buildProperties.put(BUILD_BRAND, brandOverride);
881         String deviceOverride = fingerprintPrefix.split("/")[2];
882         buildProperties.put(BUILD_DEVICE, deviceOverride);
883         String productOverride = fingerprintPrefix.split("/")[1];
884         buildProperties.put(BUILD_PRODUCT, productOverride);
885         String versionOverride = fingerprintTail.split("/")[0];
886         buildProperties.put(BUILD_VERSION_RELEASE, versionOverride);
887         buildProperties.put(BUILD_FINGERPRINT, buildFingerprintOverride);
888         buildProperties.put(BUILD_MANUFACTURER, manufactureOverride);
889         buildProperties.put(BUILD_MODEL, modelOverride);
890 
891         // Add modified values to results.
892         addBuildInfoToResult(buildProperties, mResult);
893         mResult.setBuildFingerprint(buildFingerprintOverride);
894     }
895     /** Aggregate build info from member device info. */
mapBuildInfo()896     protected Map<String, String> mapBuildInfo() {
897         Map<String, String> buildProperties = new HashMap<>();
898         for (IBuildInfo buildInfo : mMainBuildInfos) {
899             for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) {
900                 String key = entry.getKey();
901                 String value = entry.getValue();
902                 if (key.startsWith(BUILD_INFO)) {
903                     buildProperties.put(key.substring(CTS_PREFIX.length()), value);
904                 }
905             }
906         }
907         return buildProperties;
908     }
909 
910     /**
911      * Add build info to results.
912      * @param buildProperties Build info to add.
913      */
addBuildInfoToResult(Map<String, String> buildProperties, IInvocationResult invocationResult)914     protected static void addBuildInfoToResult(Map<String, String> buildProperties,
915             IInvocationResult invocationResult) {
916         buildProperties.entrySet().stream().forEach(entry ->
917                 invocationResult.addInvocationInfo(entry.getKey(), entry.getValue()));
918     }
919 
920     /**
921      * Get the suite plan. This protected method was created for overrides.
922      * Extending classes can decide on the content of the output's suite_plan field.
923      *
924      * @param mBuildHelper Helper that contains build information.
925      * @return string Suite plan to use.
926      */
getSuitePlan(CompatibilityBuildHelper mBuildHelper)927     protected String getSuitePlan(CompatibilityBuildHelper mBuildHelper) {
928         return mBuildHelper.getSuitePlan();
929     }
930 
931     /**
932      * Return true if this instance is a shard ResultReporter and should propagate certain events to
933      * the primary.
934      */
isShardResultReporter()935     private boolean isShardResultReporter() {
936         return mPrimaryResultReporter != null;
937     }
938 
939     /**
940      * When enabled, upload the result to a server.
941      */
uploadResult(File resultFile)942     private void uploadResult(File resultFile) {
943         if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) {
944             try {
945                 debug("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl));
946             } catch (IOException ioe) {
947                 CLog.e("[%s] IOException while uploading result.", mDeviceSerial);
948                 CLog.e(ioe);
949             }
950         }
951     }
952 
953     /**
954      * Returns whether it is safe to mark modules as "done", given the invocation command-line
955      * arguments. Returns true unless this is a retry and specific filtering techniques are applied
956      * on the command-line, such as:
957      *   --retry-type failed
958      *   --include-filter
959      *   --exclude-filter
960      *   -t/--test
961      *   --subplan
962      */
canMarkDone(String args)963     private boolean canMarkDone(String args) {
964         if (mRetrySessionId == null) {
965             return true; // always allow modules to be marked done if not retry
966         }
967         return !(RetryType.FAILED.equals(mRetryType)
968                 || RetryType.CUSTOM.equals(mRetryType)
969                 || args.contains(CompatibilityTestSuite.INCLUDE_FILTER_OPTION)
970                 || args.contains(CompatibilityTestSuite.EXCLUDE_FILTER_OPTION)
971                 || args.contains(CompatibilityTestSuite.SUBPLAN_OPTION)
972                 || args.matches(String.format(".* (-%s|--%s) .*",
973                 CompatibilityTestSuite.TEST_OPTION_SHORT_NAME, CompatibilityTestSuite.TEST_OPTION)));
974     }
975 
976     /**
977      * Copy the xml formatting files stored in this jar to the results directory
978      *
979      * @param resultsDir
980      */
copyFormattingFiles(File resultsDir, String suiteName)981     static void copyFormattingFiles(File resultsDir, String suiteName) {
982         for (String resultFileName : ResultHandler.RESULT_RESOURCES) {
983             InputStream configStream = ResultHandler.class.getResourceAsStream(
984                     String.format("/report/%s-%s", suiteName, resultFileName));
985             if (configStream == null) {
986                 // If suite specific files are not available, fallback to common.
987                 configStream = ResultHandler.class.getResourceAsStream(
988                     String.format("/report/%s", resultFileName));
989             }
990             if (configStream != null) {
991                 File resultFile = new File(resultsDir, resultFileName);
992                 try {
993                     FileUtil.writeToFile(configStream, resultFile);
994                 } catch (IOException e) {
995                     warn("Failed to write %s to file", resultFileName);
996                 }
997             } else {
998                 warn("Failed to load %s from jar", resultFileName);
999             }
1000         }
1001     }
1002 
1003     /**
1004      * move the dynamic config files to the results directory
1005      */
copyDynamicConfigFiles()1006     private void copyDynamicConfigFiles() {
1007         File configDir = new File(mResultDir, "config");
1008         if (!configDir.mkdir()) {
1009             warn("Failed to make dynamic config directory \"%s\" in the result",
1010                     configDir.getAbsolutePath());
1011         }
1012 
1013         Set<String> uniqueModules = new HashSet<>();
1014         for (IBuildInfo buildInfo : mMainBuildInfos) {
1015             CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo);
1016             Map<String, File> dcFiles = helper.getDynamicConfigFiles();
1017             for (String moduleName : dcFiles.keySet()) {
1018                 File srcFile = dcFiles.get(moduleName);
1019                 if (!uniqueModules.contains(moduleName)) {
1020                     // have not seen config for this module yet, copy into result
1021                     File destFile = new File(configDir, moduleName + ".dynamic");
1022                     try {
1023                         FileUtil.copyFile(srcFile, destFile);
1024                         uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds
1025                     } catch (IOException e) {
1026                         warn("Failure when copying config file \"%s\" to \"%s\" for module %s",
1027                                 srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName);
1028                         CLog.e(e);
1029                     }
1030                 }
1031                 FileUtil.deleteFile(srcFile);
1032             }
1033         }
1034     }
1035 
1036     /**
1037      * Recursively copy any other files found in the previous session's result directory to the
1038      * new result directory, so long as they don't already exist. For example, a "screenshots"
1039      * directory generated in a previous session by a passing test will not be generated on retry
1040      * unless copied from the old result directory.
1041      *
1042      * @param oldDir
1043      * @param newDir
1044      */
copyRetryFiles(File oldDir, File newDir)1045     static void copyRetryFiles(File oldDir, File newDir) {
1046         File[] oldChildren = oldDir.listFiles();
1047         for (File oldChild : oldChildren) {
1048             if (NOT_RETRY_FILES.contains(oldChild.getName())) {
1049                 continue; // do not copy this file/directory or its children
1050             }
1051             File newChild = new File(newDir, oldChild.getName());
1052             if (!newChild.exists()) {
1053                 // If this old file or directory doesn't exist in new dir, simply copy it
1054                 try {
1055                     if (oldChild.isDirectory()) {
1056                         FileUtil.recursiveCopy(oldChild, newChild);
1057                     } else {
1058                         FileUtil.copyFile(oldChild, newChild);
1059                     }
1060                 } catch (IOException e) {
1061                     warn("Failed to copy file \"%s\" from previous session", oldChild.getName());
1062                 }
1063             } else if (oldChild.isDirectory() && newChild.isDirectory()) {
1064                 // If both children exist as directories, make sure the children of the old child
1065                 // directory exist in the new child directory.
1066                 copyRetryFiles(oldChild, newChild);
1067             }
1068         }
1069     }
1070 
1071     /**
1072      * Zip the contents of the given results directory.
1073      *
1074      * @param resultsDir
1075      */
zipResults(File resultsDir)1076     private static File zipResults(File resultsDir) {
1077         File zipResultFile = null;
1078         try {
1079             // create a file in parent directory, with same name as resultsDir
1080             zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
1081                     resultsDir.getName()));
1082             ZipUtil.createZip(resultsDir, zipResultFile);
1083         } catch (IOException e) {
1084             warn("Failed to create zip for %s", resultsDir.getName());
1085         }
1086         return zipResultFile;
1087     }
1088 
1089     /**
1090      *  Log info to the console.
1091      */
info(String format, Object... args)1092     private static void info(String format, Object... args) {
1093         log(LogLevel.INFO, format, args);
1094     }
1095 
1096     /**
1097      *  Log debug to the console.
1098      */
debug(String format, Object... args)1099     private static void debug(String format, Object... args) {
1100         log(LogLevel.DEBUG, format, args);
1101     }
1102 
1103     /**
1104      *  Log a warning to the console.
1105      */
warn(String format, Object... args)1106     private static void warn(String format, Object... args) {
1107         log(LogLevel.WARN, format, args);
1108     }
1109 
1110     /**
1111      * Log a message to the console
1112      */
log(LogLevel level, String format, Object... args)1113     private static void log(LogLevel level, String format, Object... args) {
1114         CLog.logAndDisplay(level, format, args);
1115     }
1116 
1117     /**
1118      * For testing purpose.
1119      */
1120     @VisibleForTesting
getResult()1121     public IInvocationResult getResult() {
1122         return mResult;
1123     }
1124 
1125     /**
1126      * Returns true if the reporter is finalized before the end of the timeout. False otherwise.
1127      */
1128     @VisibleForTesting
waitForFinalized(long timeout, TimeUnit unit)1129     public boolean waitForFinalized(long timeout, TimeUnit unit) throws InterruptedException {
1130         return mFinalized.await(timeout, unit);
1131     }
1132 
sanitizeXmlContent(String s)1133     private static String sanitizeXmlContent(String s) {
1134         return XmlEscapers.xmlContentEscaper().escape(s);
1135     }
1136 
1137     /** Re-log a result file to all reporters so they are aware of it. */
logReportFiles( IConfiguration configuration, File resultFile, String dataName, LogDataType type)1138     private void logReportFiles(
1139             IConfiguration configuration, File resultFile, String dataName, LogDataType type) {
1140         if (configuration == null) {
1141             return;
1142         }
1143         List<ITestInvocationListener> listeners = configuration.getTestInvocationListeners();
1144         try (FileInputStreamSource source = new FileInputStreamSource(resultFile)) {
1145             for (ITestInvocationListener listener : listeners) {
1146                 if (listener.equals(this)) {
1147                     // Avoid logging agaisnt itself
1148                     continue;
1149                 }
1150                 listener.testLog(dataName, type, source);
1151             }
1152         }
1153     }
1154 }
1155