• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.compatibility.common.tradefed.result.suite;
17 
18 import com.android.annotations.VisibleForTesting;
19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
20 import com.android.compatibility.common.util.DeviceInfo;
21 import com.android.tradefed.build.IBuildInfo;
22 import com.android.tradefed.config.IConfiguration;
23 import com.android.tradefed.config.Option;
24 import com.android.tradefed.config.OptionClass;
25 import com.android.tradefed.invoker.IInvocationContext;
26 import com.android.tradefed.invoker.ShardListener;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.result.ILogSaver;
29 import com.android.tradefed.result.ITestInvocationListener;
30 import com.android.tradefed.result.ITestSummaryListener;
31 import com.android.tradefed.result.InputStreamSource;
32 import com.android.tradefed.result.LogDataType;
33 import com.android.tradefed.result.LogFile;
34 import com.android.tradefed.result.LogFileSaver;
35 import com.android.tradefed.result.SnapshotInputStreamSource;
36 import com.android.tradefed.result.TestRunResult;
37 import com.android.tradefed.result.TestSummary;
38 import com.android.tradefed.result.suite.IFormatterGenerator;
39 import com.android.tradefed.result.suite.SuiteResultReporter;
40 import com.android.tradefed.result.suite.XmlFormattedGeneratorReporter;
41 import com.android.tradefed.util.FileUtil;
42 
43 import java.io.File;
44 import java.io.FileNotFoundException;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.nio.file.Files;
48 import java.nio.file.Path;
49 import java.util.Collection;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.LinkedHashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 
57 /**
58  * Extension of {@link XmlFormattedGeneratorReporter} and {@link SuiteResultReporter} to handle
59  * Compatibility specific format and operations.
60  */
61 @OptionClass(alias = "result-reporter")
62 public class CertificationSuiteResultReporter extends XmlFormattedGeneratorReporter
63         implements ITestSummaryListener {
64 
65     // The known existing variant of suites.
66     // Adding a new variant requires approval from Android Partner team and Test Harness team.
67     private enum SuiteVariant {
68         CTS_ON_GSI("CTS_ON_GSI", "cts-on-gsi");
69 
70         private final String mReportDisplayName;
71         private final String mConfigName;
72 
SuiteVariant(String reportName, String configName)73         private SuiteVariant(String reportName, String configName) {
74             mReportDisplayName = reportName;
75             mConfigName = configName;
76         }
77 
getReportDisplayName()78         public String getReportDisplayName() {
79             return mReportDisplayName;
80         }
81 
getConfigName()82         public String getConfigName() {
83             return mConfigName;
84         }
85     }
86 
87     public static final String LATEST_LINK_NAME = "latest";
88     public static final String SUMMARY_FILE = "invocation_summary.txt";
89 
90     public static final String BUILD_FINGERPRINT = "cts:build_fingerprint";
91 
92     @Option(name = "result-server", description = "Server to publish test results.")
93     @Deprecated
94     private String mResultServer;
95 
96     @Option(
97             name = "disable-result-posting",
98             description = "Disable result posting into report server.")
99     @Deprecated
100     private boolean mDisableResultPosting = false;
101 
102     @Option(name = "include-test-log-tags", description = "Include test log tags in report.")
103     private boolean mIncludeTestLogTags = false;
104 
105     @Option(name = "use-log-saver", description = "Also saves generated result with log saver")
106     private boolean mUseLogSaver = false;
107 
108     @Option(name = "compress-logs", description = "Whether logs will be saved with compression")
109     private boolean mCompressLogs = true;
110 
111     public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip";
112 
113     @Option(
114             name = INCLUDE_HTML_IN_ZIP,
115             description = "Whether failure summary report is included in the zip fie.")
116     @Deprecated
117     private boolean mIncludeHtml = false;
118 
119     @Option(
120             name = "result-attribute",
121             description =
122                     "Extra key-value pairs to be added as attributes and corresponding values "
123                             + "of the \"Result\" tag in the result XML.")
124     private Map<String, String> mResultAttributes = new HashMap<String, String>();
125 
126     // Should be removed for the S release.
127     @Option(
128             name = "cts-on-gsi-variant",
129             description =
130                     "Workaround for the R release to ensure the CTS-on-GSI report can be parsed "
131                             + "by the APFE.")
132     private boolean mCtsOnGsiVariant = false;
133 
134     private CompatibilityBuildHelper mBuildHelper;
135 
136     /** The directory containing the results */
137     private File mResultDir = null;
138     /** The directory containing the logs */
139     private File mLogDir = null;
140 
141     /** LogFileSaver to copy the file to the CTS results folder */
142     private LogFileSaver mTestLogSaver;
143 
144     private Map<LogFile, InputStreamSource> mPreInvocationLogs = new HashMap<>();
145     /** Invocation level Log saver to receive when files are logged */
146     private ILogSaver mLogSaver;
147 
148     private String mReferenceUrl;
149 
150     private Map<String, String> mLoggedFiles;
151 
152     private static final String[] RESULT_RESOURCES = {
153         "compatibility_result.css",
154         "compatibility_result.xsl",
155         "logo.png"
156     };
157 
CertificationSuiteResultReporter()158     public CertificationSuiteResultReporter() {
159         super();
160         mLoggedFiles = new LinkedHashMap<>();
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
invocationStarted(IInvocationContext context)167     public final void invocationStarted(IInvocationContext context) {
168         super.invocationStarted(context);
169 
170         if (mBuildHelper == null) {
171             mBuildHelper = createBuildHelper();
172         }
173         if (mResultDir == null) {
174             initializeResultDirectories();
175         }
176     }
177 
178     @VisibleForTesting
createBuildHelper()179     CompatibilityBuildHelper createBuildHelper() {
180         return new CompatibilityBuildHelper(getPrimaryBuildInfo());
181     }
182 
183     /**
184      * {@inheritDoc}
185      */
186     @Override
testLog(String name, LogDataType type, InputStreamSource stream)187     public void testLog(String name, LogDataType type, InputStreamSource stream) {
188         if (name.endsWith(DeviceInfo.FILE_SUFFIX)) {
189             // Handle device info file case
190             testLogDeviceInfo(name, stream);
191             return;
192         }
193         if (mTestLogSaver == null) {
194             LogFile info = new LogFile(name, null, type);
195             mPreInvocationLogs.put(
196                     info, new SnapshotInputStreamSource(name, stream.createInputStream()));
197             return;
198         }
199         try {
200             File logFile = null;
201             if (mCompressLogs) {
202                 try (InputStream inputStream = stream.createInputStream()) {
203                     logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream);
204                 }
205             } else {
206                 try (InputStream inputStream = stream.createInputStream()) {
207                     logFile = mTestLogSaver.saveLogData(name, type, inputStream);
208                 }
209             }
210             CLog.d("Saved logs for %s in %s", name, logFile.getAbsolutePath());
211         } catch (IOException e) {
212             CLog.e("Failed to write log for %s", name);
213             CLog.e(e);
214         }
215     }
216 
217     /** Write device-info files to the result */
testLogDeviceInfo(String name, InputStreamSource stream)218     private void testLogDeviceInfo(String name, InputStreamSource stream) {
219         try {
220             File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME);
221             ediDir.mkdirs();
222             File ediFile = new File(ediDir, name);
223             if (!ediFile.exists()) {
224                 // only write this file to the results if not already present
225                 FileUtil.writeToFile(stream.createInputStream(), ediFile);
226             }
227         } catch (IOException e) {
228             CLog.w("Failed to write device info %s to result", name);
229             CLog.e(e);
230         }
231     }
232 
233     /**
234      * {@inheritDoc}
235      */
236     @Override
testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)237     public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
238             LogFile logFile) {
239         if (mIncludeTestLogTags) {
240             switch (dataType) {
241                 case BUGREPORT:
242                 case LOGCAT:
243                 case PNG:
244                     mLoggedFiles.put(dataName, logFile.getUrl());
245                     break;
246                 default:
247                     // Do nothing
248                     break;
249             }
250         }
251     }
252 
253     /**
254      * {@inheritDoc}
255      */
256     @Override
putSummary(List<TestSummary> summaries)257     public void putSummary(List<TestSummary> summaries) {
258         for (TestSummary summary : summaries) {
259             if (mReferenceUrl == null && summary.getSummary().getString() != null) {
260                 mReferenceUrl = summary.getSummary().getString();
261             }
262         }
263     }
264 
265     /**
266      * {@inheritDoc}
267      */
268     @Override
setLogSaver(ILogSaver saver)269     public void setLogSaver(ILogSaver saver) {
270         mLogSaver = saver;
271     }
272 
273     /**
274      * Create directory structure where results and logs will be written.
275      */
initializeResultDirectories()276     private void initializeResultDirectories() {
277         CLog.d("Initializing result directory");
278         try {
279             mResultDir = mBuildHelper.getResultDir();
280             if (mResultDir != null) {
281                 mResultDir.mkdirs();
282             }
283         } catch (FileNotFoundException e) {
284             throw new RuntimeException(e);
285         }
286 
287         if (mResultDir == null) {
288             throw new RuntimeException("Result Directory was not created");
289         }
290         if (!mResultDir.exists()) {
291             throw new RuntimeException("Result Directory was not created: " +
292                     mResultDir.getAbsolutePath());
293         }
294 
295         CLog.d("Results Directory: %s", mResultDir.getAbsolutePath());
296 
297         try {
298             mLogDir = mBuildHelper.getInvocationLogDir();
299         } catch (FileNotFoundException e) {
300             CLog.e(e);
301         }
302         if (mLogDir != null && mLogDir.mkdirs()) {
303             CLog.d("Created log dir %s", mLogDir.getAbsolutePath());
304         }
305         if (mLogDir == null || !mLogDir.exists()) {
306             throw new IllegalArgumentException(String.format("Could not create log dir %s",
307                     mLogDir.getAbsolutePath()));
308         }
309         // During sharding, we reach here before invocationStarted is called so the log_saver will
310         // be null at that point.
311         if (mTestLogSaver == null) {
312             mTestLogSaver = new LogFileSaver(mLogDir);
313             // Log all the early logs from before init.
314             for (LogFile earlyLog : mPreInvocationLogs.keySet()) {
315                 try (InputStreamSource source = mPreInvocationLogs.get(earlyLog)) {
316                     testLog(earlyLog.getPath(), earlyLog.getType(), source);
317                 }
318             }
319             mPreInvocationLogs.clear();
320         }
321     }
322 
323     @Override
createFormatter()324     public IFormatterGenerator createFormatter() {
325         return new CertificationResultXml(
326                 mBuildHelper.getSuiteName(),
327                 mBuildHelper.getSuiteVersion(),
328                 createSuiteVariant(),
329                 mBuildHelper.getSuitePlan(),
330                 mBuildHelper.getSuiteBuild(),
331                 mReferenceUrl,
332                 getLogUrl(),
333                 mResultAttributes);
334     }
335 
336     @Override
preFormattingSetup(IFormatterGenerator formater)337     public void preFormattingSetup(IFormatterGenerator formater) {
338         super.preFormattingSetup(formater);
339         // Log the summary
340         TestSummary summary = getSummary();
341         try {
342             File summaryFile = new File(mResultDir, SUMMARY_FILE);
343             FileUtil.writeToFile(summary.getSummary().toString(), summaryFile);
344         } catch (IOException e) {
345             CLog.e("Failed to save the summary.");
346             CLog.e(e);
347         }
348 
349         copyDynamicConfigFiles();
350         copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName());
351     }
352 
353     @Override
createResultDir()354     public File createResultDir() throws IOException {
355         return mResultDir;
356     }
357 
358     @Override
postFormattingStep(File resultDir, File reportFile)359     public void postFormattingStep(File resultDir, File reportFile) {
360         super.postFormattingStep(resultDir,reportFile);
361 
362         createChecksum(
363                 resultDir,
364                 getMergedTestRunResults(),
365                 getPrimaryBuildInfo().getBuildAttributes().get(BUILD_FINGERPRINT));
366 
367         Path latestLink = createLatestLinkDirectory(mResultDir.toPath());
368         if (latestLink != null) {
369             CLog.i("Latest results link: " + latestLink.toAbsolutePath());
370         }
371 
372         latestLink = createLatestLinkDirectory(mLogDir.toPath());
373         if (latestLink != null) {
374             CLog.i("Latest logs link: " + latestLink.toAbsolutePath());
375         }
376 
377         for (ITestInvocationListener resultReporter :
378                 getConfiguration().getTestInvocationListeners()) {
379             if (resultReporter instanceof CertificationReportCreator) {
380                 ((CertificationReportCreator) resultReporter).setReportFile(reportFile);
381             }
382             if (resultReporter instanceof ShardListener) {
383                 for (ITestInvocationListener subListener : ((ShardListener) resultReporter).getUnderlyingResultReporter()) {
384                     if (subListener instanceof CertificationReportCreator) {
385                         ((CertificationReportCreator) subListener).setReportFile(reportFile);
386                     }
387                 }
388             }
389         }
390     }
391 
392     /**
393      * Return the path in which log saver persists log files or null if
394      * logSaver is not enabled.
395      */
getLogUrl()396     private String getLogUrl() {
397         if (!mUseLogSaver || mLogSaver == null) {
398             return null;
399         }
400 
401         return mLogSaver.getLogReportDir().getUrl();
402     }
403 
404     /**
405      * Update the "latest" symlink to the newest result directory. CTS specific.
406      */
createLatestLinkDirectory(Path directory)407     private Path createLatestLinkDirectory(Path directory) {
408         Path link = null;
409 
410         Path parent = directory.getParent();
411 
412         if (parent != null) {
413             link = parent.resolve(LATEST_LINK_NAME);
414             try {
415                 // if latest already exists, we have to remove it before creating
416                 Files.deleteIfExists(link);
417                 Files.createSymbolicLink(link, directory);
418             } catch (IOException ioe) {
419                 CLog.e("Exception while attempting to create 'latest' link to: [%s]",
420                     directory);
421                 CLog.e(ioe);
422                 return null;
423             } catch (UnsupportedOperationException uoe) {
424                 CLog.e("Failed to create 'latest' symbolic link - unsupported operation");
425                 return null;
426             }
427         }
428         return link;
429     }
430 
431     /**
432      * move the dynamic config files to the results directory
433      */
copyDynamicConfigFiles()434     private void copyDynamicConfigFiles() {
435         File configDir = new File(mResultDir, "config");
436         if (!configDir.exists() && !configDir.mkdir()) {
437             CLog.w(
438                     "Failed to make dynamic config directory \"%s\" in the result.",
439                     configDir.getAbsolutePath());
440         }
441 
442         Set<String> uniqueModules = new HashSet<>();
443         // Check each build of the invocation, in case of multi-device invocation.
444         for (IBuildInfo buildInfo : getInvocationContext().getBuildInfos()) {
445             CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo);
446             Map<String, File> dcFiles = helper.getDynamicConfigFiles();
447             for (String moduleName : dcFiles.keySet()) {
448                 File srcFile = dcFiles.get(moduleName);
449                 if (!uniqueModules.contains(moduleName)) {
450                     // have not seen config for this module yet, copy into result
451                     File destFile = new File(configDir, moduleName + ".dynamic");
452                     if (destFile.exists()) {
453                         continue;
454                     }
455                     try {
456                         FileUtil.copyFile(srcFile, destFile);
457                         uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds
458                     } catch (IOException e) {
459                         CLog.w("Failure when copying config file \"%s\" to \"%s\" for module %s",
460                                 srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName);
461                         CLog.e(e);
462                     }
463                 }
464                 FileUtil.deleteFile(srcFile);
465             }
466         }
467     }
468 
469     /**
470      * Copy the xml formatting files stored in this jar to the results directory. CTS specific.
471      *
472      * @param resultsDir
473      */
copyFormattingFiles(File resultsDir, String suiteName)474     private void copyFormattingFiles(File resultsDir, String suiteName) {
475         for (String resultFileName : RESULT_RESOURCES) {
476             InputStream configStream = CertificationResultXml.class.getResourceAsStream(
477                     String.format("/report/%s-%s", suiteName, resultFileName));
478             if (configStream == null) {
479                 // If suite specific files are not available, fallback to common.
480                 configStream = CertificationResultXml.class.getResourceAsStream(
481                     String.format("/report/%s", resultFileName));
482             }
483             if (configStream != null) {
484                 File resultFile = new File(resultsDir, resultFileName);
485                 try {
486                     FileUtil.writeToFile(configStream, resultFile);
487                 } catch (IOException e) {
488                     CLog.w("Failed to write %s to file", resultFileName);
489                 }
490             } else {
491                 CLog.w("Failed to load %s from jar", resultFileName);
492             }
493         }
494     }
495 
496     /**
497      * Generates a checksum files based on the results.
498      */
createChecksum(File resultDir, Collection<TestRunResult> results, String buildFingerprint)499     private void createChecksum(File resultDir, Collection<TestRunResult> results,
500             String buildFingerprint) {
501         CertificationChecksumHelper.tryCreateChecksum(resultDir, results, buildFingerprint);
502     }
503 
createSuiteVariant()504     private String createSuiteVariant() {
505         IConfiguration currentConfig = getConfiguration();
506         String commandLine = currentConfig.getCommandLine();
507         for (SuiteVariant var : SuiteVariant.values()) {
508             if (commandLine.startsWith(var.getConfigName() + " ")
509                     || commandLine.equals(var.getConfigName())) {
510                 return var.getReportDisplayName();
511             }
512         }
513         return null;
514     }
515 }
516