• 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.compatibility.common.tradefed.build.CompatibilityBuildHelper;
19 import com.android.compatibility.common.util.DeviceInfo;
20 import com.android.compatibility.common.util.ResultHandler;
21 import com.android.compatibility.common.util.ResultUploader;
22 import com.android.tradefed.build.IBuildInfo;
23 import com.android.tradefed.config.IConfiguration;
24 import com.android.tradefed.config.IConfigurationReceiver;
25 import com.android.tradefed.config.Option;
26 import com.android.tradefed.config.OptionClass;
27 import com.android.tradefed.invoker.IInvocationContext;
28 import com.android.tradefed.log.LogUtil.CLog;
29 import com.android.tradefed.result.FileInputStreamSource;
30 import com.android.tradefed.result.ILogSaver;
31 import com.android.tradefed.result.ITestInvocationListener;
32 import com.android.tradefed.result.ITestSummaryListener;
33 import com.android.tradefed.result.InputStreamSource;
34 import com.android.tradefed.result.LogDataType;
35 import com.android.tradefed.result.LogFile;
36 import com.android.tradefed.result.LogFileSaver;
37 import com.android.tradefed.result.TestRunResult;
38 import com.android.tradefed.result.TestSummary;
39 import com.android.tradefed.result.suite.IFormatterGenerator;
40 import com.android.tradefed.result.suite.SuiteResultReporter;
41 import com.android.tradefed.result.suite.XmlFormattedGeneratorReporter;
42 import com.android.tradefed.util.FileUtil;
43 import com.android.tradefed.util.StreamUtil;
44 import com.android.tradefed.util.ZipUtil;
45 
46 import java.io.File;
47 import java.io.FileInputStream;
48 import java.io.FileNotFoundException;
49 import java.io.FileOutputStream;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.io.OutputStream;
53 import java.nio.file.Files;
54 import java.nio.file.Path;
55 import java.util.Collection;
56 import java.util.HashSet;
57 import java.util.LinkedHashMap;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Set;
61 
62 import javax.xml.transform.Transformer;
63 import javax.xml.transform.TransformerException;
64 import javax.xml.transform.TransformerFactory;
65 import javax.xml.transform.stream.StreamResult;
66 import javax.xml.transform.stream.StreamSource;
67 
68 /**
69  * Extension of {@link XmlFormattedGeneratorReporter} and {@link SuiteResultReporter} to handle
70  * Compatibility specific format and operations.
71  */
72 @OptionClass(alias = "result-reporter")
73 public class CertificationSuiteResultReporter extends XmlFormattedGeneratorReporter
74         implements IConfigurationReceiver, ITestSummaryListener {
75 
76     public static final String LATEST_LINK_NAME = "latest";
77     public static final String SUMMARY_FILE = "invocation_summary.txt";
78     public static final String FAILURE_REPORT_NAME = "test_result_failures_suite.html";
79     public static final String FAILURE_XSL_FILE_NAME = "compatibility_failures.xsl";
80 
81     public static final String BUILD_FINGERPRINT = "build_fingerprint";
82 
83     @Option(name = "result-server", description = "Server to publish test results.")
84     private String mResultServer;
85 
86     @Option(
87             name = "disable-result-posting",
88             description ="Disable result posting into report server."
89     )
90     private boolean mDisableResultPosting = false;
91 
92     @Option(name = "include-test-log-tags", description = "Include test log tags in report.")
93     private boolean mIncludeTestLogTags = false;
94 
95     @Option(name = "use-log-saver", description = "Also saves generated result with log saver")
96     private boolean mUseLogSaver = false;
97 
98     @Option(name = "compress-logs", description = "Whether logs will be saved with compression")
99     private boolean mCompressLogs = true;
100 
101     public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip";
102     @Option(name = INCLUDE_HTML_IN_ZIP,
103             description = "Whether failure summary report is included in the zip fie.")
104     private boolean mIncludeHtml = false;
105 
106     private CompatibilityBuildHelper mBuildHelper;
107 
108     /** The directory containing the results */
109     private File mResultDir = null;
110     /** The directory containing the logs */
111     private File mLogDir = null;
112 
113     private ResultUploader mUploader;
114 
115     private LogFileSaver mTestLogSaver;
116     /** Invocation level Log saver to receive when files are logged */
117     private ILogSaver mLogSaver;
118     /** Invocation level configuration */
119     private IConfiguration mConfiguration = null;
120 
121     private String mReferenceUrl;
122 
123     private Map<String, String> mLoggedFiles;
124 
125     private static final String[] RESULT_RESOURCES = {
126         "compatibility_result.css",
127         "compatibility_result.xsd",
128         "compatibility_result.xsl",
129         "logo.png"
130     };
131 
CertificationSuiteResultReporter()132     public CertificationSuiteResultReporter() {
133         super();
134         mLoggedFiles = new LinkedHashMap<>();
135     }
136 
137     /**
138      * {@inheritDoc}
139      */
140     @Override
invocationStarted(IInvocationContext context)141     public final void invocationStarted(IInvocationContext context) {
142         super.invocationStarted(context);
143 
144         if (mBuildHelper == null) {
145             mBuildHelper = new CompatibilityBuildHelper(getPrimaryBuildInfo());
146         }
147         if (mResultDir == null) {
148             initializeResultDirectories();
149         }
150     }
151 
152     /**
153      * {@inheritDoc}
154      */
155     @Override
testLog(String name, LogDataType type, InputStreamSource stream)156     public void testLog(String name, LogDataType type, InputStreamSource stream) {
157         if (name.endsWith(DeviceInfo.FILE_SUFFIX)) {
158             // Handle device info file case
159             testLogDeviceInfo(name, stream);
160             return;
161         }
162         try {
163             File logFile = null;
164             if (mCompressLogs) {
165                 try (InputStream inputStream = stream.createInputStream()) {
166                     logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream);
167                 }
168             } else {
169                 try (InputStream inputStream = stream.createInputStream()) {
170                     logFile = mTestLogSaver.saveLogData(name, type, inputStream);
171                 }
172             }
173             CLog.d("Saved logs for %s in %s", name, logFile.getAbsolutePath());
174         } catch (IOException e) {
175             CLog.e("Failed to write log for %s", name);
176             CLog.e(e);
177         }
178     }
179 
180     /** Write device-info files to the result, invoked only by the master result reporter */
testLogDeviceInfo(String name, InputStreamSource stream)181     private void testLogDeviceInfo(String name, InputStreamSource stream) {
182         try {
183             File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME);
184             ediDir.mkdirs();
185             File ediFile = new File(ediDir, name);
186             if (!ediFile.exists()) {
187                 // only write this file to the results if not already present
188                 FileUtil.writeToFile(stream.createInputStream(), ediFile);
189             }
190         } catch (IOException e) {
191             CLog.w("Failed to write device info %s to result", name);
192             CLog.e(e);
193         }
194     }
195 
196     /**
197      * {@inheritDoc}
198      */
199     @Override
testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)200     public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
201             LogFile logFile) {
202         if (mIncludeTestLogTags) {
203             switch (dataType) {
204                 case BUGREPORT:
205                 case LOGCAT:
206                 case PNG:
207                     mLoggedFiles.put(dataName, logFile.getUrl());
208                     break;
209                 default:
210                     // Do nothing
211                     break;
212             }
213         }
214     }
215 
216     /**
217      * {@inheritDoc}
218      */
219     @Override
putSummary(List<TestSummary> summaries)220     public void putSummary(List<TestSummary> summaries) {
221         for (TestSummary summary : summaries) {
222             if (mReferenceUrl == null && summary.getSummary().getString() != null) {
223                 mReferenceUrl = summary.getSummary().getString();
224             }
225         }
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
setLogSaver(ILogSaver saver)232     public void setLogSaver(ILogSaver saver) {
233         mLogSaver = saver;
234     }
235 
236     /** {@inheritDoc} */
237     @Override
setConfiguration(IConfiguration configuration)238     public void setConfiguration(IConfiguration configuration) {
239         mConfiguration = configuration;
240     }
241 
242     /**
243      * Create directory structure where results and logs will be written.
244      */
initializeResultDirectories()245     private void initializeResultDirectories() {
246         CLog.d("Initializing result directory");
247         // TODO: Clean up start time handling to avoid relying on buildinfo
248         getPrimaryBuildInfo().addBuildAttribute(CompatibilityBuildHelper.START_TIME_MS,
249                 Long.toString(getStartTime()));
250         try {
251             mResultDir = mBuildHelper.getResultDir();
252             if (mResultDir != null) {
253                 mResultDir.mkdirs();
254             }
255         } catch (FileNotFoundException e) {
256             throw new RuntimeException(e);
257         }
258 
259         if (mResultDir == null) {
260             throw new RuntimeException("Result Directory was not created");
261         }
262         if (!mResultDir.exists()) {
263             throw new RuntimeException("Result Directory was not created: " +
264                     mResultDir.getAbsolutePath());
265         }
266 
267         CLog.d("Results Directory: %s", mResultDir.getAbsolutePath());
268 
269         mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName());
270         try {
271             mLogDir = new File(mBuildHelper.getLogsDir(),
272                     CompatibilityBuildHelper.getDirSuffix(getStartTime()));
273         } catch (FileNotFoundException e) {
274             CLog.e(e);
275         }
276         if (mLogDir != null && mLogDir.mkdirs()) {
277             CLog.d("Created log dir %s", mLogDir.getAbsolutePath());
278         }
279         if (mLogDir == null || !mLogDir.exists()) {
280             throw new IllegalArgumentException(String.format("Could not create log dir %s",
281                     mLogDir.getAbsolutePath()));
282         }
283         if (mTestLogSaver == null) {
284             mTestLogSaver = new LogFileSaver(mLogDir);
285         }
286     }
287 
288     @Override
createFormatter()289     public IFormatterGenerator createFormatter() {
290         return new CertificationResultXml(mBuildHelper.getSuiteName(),
291                 mBuildHelper.getSuiteVersion(),
292                 mBuildHelper.getSuitePlan(),
293                 mBuildHelper.getSuiteBuild(),
294                 mReferenceUrl,
295                 getLogUrl());
296     }
297 
298     @Override
preFormattingSetup(IFormatterGenerator formater)299     public void preFormattingSetup(IFormatterGenerator formater) {
300         super.preFormattingSetup(formater);
301         // Log the summary
302         TestSummary summary = getSummary();
303         try {
304             File summaryFile = new File(mResultDir, SUMMARY_FILE);
305             FileUtil.writeToFile(summary.getSummary().toString(), summaryFile);
306         } catch (IOException e) {
307             CLog.e("Failed to save the summary.");
308             CLog.e(e);
309         }
310 
311         copyDynamicConfigFiles();
312         copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName());
313     }
314 
315     @Override
createResultDir()316     public File createResultDir() throws IOException {
317         return mResultDir;
318     }
319 
320     @Override
postFormattingStep(File resultDir, File reportFile)321     public void postFormattingStep(File resultDir, File reportFile) {
322         super.postFormattingStep(resultDir,reportFile);
323 
324         createChecksum(resultDir, getRunResults(),
325                 getPrimaryBuildInfo().getBuildAttributes().get(BUILD_FINGERPRINT));
326 
327         File failureReport = null;
328         if (mIncludeHtml) {
329             // Create the html report before the zip file.
330             failureReport = createFailureReport(reportFile);
331         }
332         File zippedResults = zipResults(mResultDir);
333         // TODO: calculate results checksum file
334         if (!mIncludeHtml) {
335             // Create failure report after zip file so extra data is not uploaded
336             failureReport = createFailureReport(reportFile);
337         }
338         try {
339             if (failureReport.exists()) {
340                 CLog.i("Test Result: %s", failureReport.getCanonicalPath());
341             } else {
342                 CLog.i("Test Result: %s", reportFile.getCanonicalPath());
343             }
344             Path latestLink = createLatestLinkDirectory(mResultDir.toPath());
345             if (latestLink != null) {
346                 CLog.i("Latest results link: " + latestLink.toAbsolutePath());
347             }
348 
349             latestLink = createLatestLinkDirectory(mLogDir.toPath());
350             if (latestLink != null) {
351                 CLog.i("Latest logs link: " + latestLink.toAbsolutePath());
352             }
353 
354             saveLog(reportFile, zippedResults);
355         } catch (IOException e) {
356             CLog.e("Error when handling the post processing of results file:");
357             CLog.e(e);
358         }
359 
360         uploadResult(reportFile);
361     }
362 
363     /**
364      * Return the path in which log saver persists log files or null if
365      * logSaver is not enabled.
366      */
getLogUrl()367     private String getLogUrl() {
368         if (!mUseLogSaver || mLogSaver == null) {
369             return null;
370         }
371 
372         return mLogSaver.getLogReportDir().getUrl();
373     }
374 
375     /**
376      * Update the "latest" symlink to the newest result directory. CTS specific.
377      */
createLatestLinkDirectory(Path directory)378     private Path createLatestLinkDirectory(Path directory) {
379         Path link = null;
380 
381         Path parent = directory.getParent();
382 
383         if (parent != null) {
384             link = parent.resolve(LATEST_LINK_NAME);
385             try {
386                 // if latest already exists, we have to remove it before creating
387                 Files.deleteIfExists(link);
388                 Files.createSymbolicLink(link, directory);
389             } catch (IOException ioe) {
390                 CLog.e("Exception while attempting to create 'latest' link to: [%s]",
391                     directory);
392                 CLog.e(ioe);
393                 return null;
394             } catch (UnsupportedOperationException uoe) {
395                 CLog.e("Failed to create 'latest' symbolic link - unsupported operation");
396                 return null;
397             }
398         }
399         return link;
400     }
401 
402     /**
403      * move the dynamic config files to the results directory
404      */
copyDynamicConfigFiles()405     private void copyDynamicConfigFiles() {
406         File configDir = new File(mResultDir, "config");
407         if (!configDir.mkdir()) {
408             CLog.w("Failed to make dynamic config directory \"%s\" in the result",
409                     configDir.getAbsolutePath());
410         }
411 
412         Set<String> uniqueModules = new HashSet<>();
413         // Check each build of the invocation, in case of multi-device invocation.
414         for (IBuildInfo buildInfo : getInvocationContext().getBuildInfos()) {
415             CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo);
416             Map<String, File> dcFiles = helper.getDynamicConfigFiles();
417             for (String moduleName : dcFiles.keySet()) {
418                 File srcFile = dcFiles.get(moduleName);
419                 if (!uniqueModules.contains(moduleName)) {
420                     // have not seen config for this module yet, copy into result
421                     File destFile = new File(configDir, moduleName + ".dynamic");
422                     try {
423                         FileUtil.copyFile(srcFile, destFile);
424                         uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds
425                     } catch (IOException e) {
426                         CLog.w("Failure when copying config file \"%s\" to \"%s\" for module %s",
427                                 srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName);
428                         CLog.e(e);
429                     }
430                 }
431                 FileUtil.deleteFile(srcFile);
432             }
433         }
434     }
435 
436     /**
437      * Copy the xml formatting files stored in this jar to the results directory. CTS specific.
438      *
439      * @param resultsDir
440      */
copyFormattingFiles(File resultsDir, String suiteName)441     private void copyFormattingFiles(File resultsDir, String suiteName) {
442         for (String resultFileName : RESULT_RESOURCES) {
443             InputStream configStream = CertificationResultXml.class.getResourceAsStream(
444                     String.format("/report/%s-%s", suiteName, resultFileName));
445             if (configStream == null) {
446                 // If suite specific files are not available, fallback to common.
447                 configStream = CertificationResultXml.class.getResourceAsStream(
448                     String.format("/report/%s", resultFileName));
449             }
450             if (configStream != null) {
451                 File resultFile = new File(resultsDir, resultFileName);
452                 try {
453                     FileUtil.writeToFile(configStream, resultFile);
454                 } catch (IOException e) {
455                     CLog.w("Failed to write %s to file", resultFileName);
456                 }
457             } else {
458                 CLog.w("Failed to load %s from jar", resultFileName);
459             }
460         }
461     }
462 
463     /**
464      * When enabled, save log data using log saver
465      */
saveLog(File resultFile, File zippedResults)466     private void saveLog(File resultFile, File zippedResults) throws IOException {
467         if (!mUseLogSaver) {
468             return;
469         }
470 
471         FileInputStream fis = null;
472         LogFile logFile = null;
473         try {
474             fis = new FileInputStream(resultFile);
475             logFile = mLogSaver.saveLogData("log-result", LogDataType.XML, fis);
476             CLog.d("Result XML URL: %s", logFile.getUrl());
477             logReportFiles(mConfiguration, resultFile, resultFile.getName(), LogDataType.XML);
478         } catch (IOException ioe) {
479             CLog.e("error saving XML with log saver");
480             CLog.e(ioe);
481         } finally {
482             StreamUtil.close(fis);
483         }
484         // Save the full results folder.
485         if (zippedResults != null) {
486             FileInputStream zipResultStream = null;
487             try {
488                 zipResultStream = new FileInputStream(zippedResults);
489                 logFile = mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream);
490                 CLog.d("Result zip URL: %s", logFile.getUrl());
491                 logReportFiles(mConfiguration, zippedResults, "results", LogDataType.ZIP);
492             } finally {
493                 StreamUtil.close(zipResultStream);
494             }
495         }
496     }
497 
498     /**
499      * Zip the contents of the given results directory. CTS specific.
500      *
501      * @param resultsDir
502      */
zipResults(File resultsDir)503     private static File zipResults(File resultsDir) {
504         File zipResultFile = null;
505         try {
506             // create a file in parent directory, with same name as resultsDir
507             zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
508                     resultsDir.getName()));
509             ZipUtil.createZip(resultsDir, zipResultFile);
510         } catch (IOException e) {
511             CLog.w("Failed to create zip for %s", resultsDir.getName());
512         }
513         return zipResultFile;
514     }
515 
516     /**
517      * When enabled, upload the result to a server. CTS specific.
518      */
uploadResult(File resultFile)519     private void uploadResult(File resultFile) {
520         if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) {
521             try {
522                 CLog.d("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl));
523             } catch (IOException ioe) {
524                 CLog.e("IOException while uploading result.");
525                 CLog.e(ioe);
526             }
527         }
528     }
529 
530     /**
531      * Generate html report listing an failed tests. CTS specific.
532      */
createFailureReport(File inputXml)533     private File createFailureReport(File inputXml) {
534         File failureReport = new File(inputXml.getParentFile(), FAILURE_REPORT_NAME);
535         try (InputStream xslStream = ResultHandler.class.getResourceAsStream(
536                 String.format("/report/%s", FAILURE_XSL_FILE_NAME));
537              OutputStream outputStream = new FileOutputStream(failureReport)) {
538 
539             Transformer transformer = TransformerFactory.newInstance().newTransformer(
540                     new StreamSource(xslStream));
541             transformer.transform(new StreamSource(inputXml), new StreamResult(outputStream));
542         } catch (IOException | TransformerException ignored) {
543             CLog.e(ignored);
544         }
545         return failureReport;
546     }
547 
548     /**
549      * Generates a checksum files based on the results.
550      */
createChecksum(File resultDir, Collection<TestRunResult> results, String buildFingerprint)551     private void createChecksum(File resultDir, Collection<TestRunResult> results,
552             String buildFingerprint) {
553         CertificationChecksumHelper.tryCreateChecksum(resultDir, results, buildFingerprint);
554     }
555 
556     /** Re-log a result file to all reporters so they are aware of it. */
logReportFiles( IConfiguration configuration, File resultFile, String dataName, LogDataType type)557     private void logReportFiles(
558             IConfiguration configuration, File resultFile, String dataName, LogDataType type) {
559         if (configuration == null) {
560             return;
561         }
562         List<ITestInvocationListener> listeners = configuration.getTestInvocationListeners();
563         try (FileInputStreamSource source = new FileInputStreamSource(resultFile)) {
564             for (ITestInvocationListener listener : listeners) {
565                 if (listener.equals(this)) {
566                     // Avoid logging agaisnt itself
567                     continue;
568                 }
569                 listener.testLog(dataName, type, source);
570             }
571         }
572     }
573 }
574